reactifact 0.11.2__tar.gz → 0.12.0__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 (245) hide show
  1. {reactifact-0.11.2/reactifact.egg-info → reactifact-0.12.0}/PKG-INFO +1 -1
  2. {reactifact-0.11.2 → reactifact-0.12.0}/pyproject.toml +1 -1
  3. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/__init__.py +1 -1
  4. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/resources.py +6 -0
  5. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/__init__.py +2 -1
  6. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/assertions.py +9 -1
  7. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/lab.py +106 -1
  8. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/__init__.py +2 -0
  9. reactifact-0.12.0/reactifact/tracing/columns.py +169 -0
  10. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/postgres.py +124 -0
  11. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/store.py +120 -1
  12. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/templates/app.css +22 -5
  13. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/templates/ui.html +48 -4
  14. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/templates/ui_run.html +13 -0
  15. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/tracer.py +9 -4
  16. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/web.py +17 -1
  17. {reactifact-0.11.2 → reactifact-0.12.0/reactifact.egg-info}/PKG-INFO +1 -1
  18. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact.egg-info/SOURCES.txt +2 -0
  19. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_devops_web.py +54 -0
  20. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_repair_web.py +26 -0
  21. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_testing_lab.py +91 -0
  22. reactifact-0.12.0/tests/test_trace_columns.py +388 -0
  23. {reactifact-0.11.2 → reactifact-0.12.0}/LICENSE +0 -0
  24. {reactifact-0.11.2 → reactifact-0.12.0}/README.md +0 -0
  25. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/__main__.py +0 -0
  26. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/_extras.py +0 -0
  27. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/_httpx.py +0 -0
  28. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/agent_tool.py +0 -0
  29. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/agents.py +0 -0
  30. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/artifacts.py +0 -0
  31. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/audit.py +0 -0
  32. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/branching.py +0 -0
  33. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/budget.py +0 -0
  34. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/chat.py +0 -0
  35. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/checkpoints.py +0 -0
  36. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/__init__.py +0 -0
  37. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/branch.py +0 -0
  38. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/common.py +0 -0
  39. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/context.py +0 -0
  40. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/graph.py +0 -0
  41. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/replay.py +0 -0
  42. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/scenario.py +0 -0
  43. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/cli/trace.py +0 -0
  44. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/commit.py +0 -0
  45. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/commit_log.py +0 -0
  46. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/consume.py +0 -0
  47. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/context.py +0 -0
  48. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/context_builder.py +0 -0
  49. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/effects.py +0 -0
  50. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/eval.py +0 -0
  51. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/events.py +0 -0
  52. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/interrupt.py +0 -0
  53. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/llm_agent.py +0 -0
  54. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/mcp/__init__.py +0 -0
  55. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/mcp/client.py +0 -0
  56. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/mcp/oauth.py +0 -0
  57. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/mcp/server.py +0 -0
  58. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/native_tool_use.py +0 -0
  59. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/operations.py +0 -0
  60. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/patches.py +0 -0
  61. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/produce.py +0 -0
  62. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/prompts.py +0 -0
  63. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/__init__.py +0 -0
  64. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/_retry.py +0 -0
  65. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/anthropic.py +0 -0
  66. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/azure.py +0 -0
  67. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/cerebras.py +0 -0
  68. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/chat.py +0 -0
  69. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/contracts.py +0 -0
  70. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/deepseek.py +0 -0
  71. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/fake.py +0 -0
  72. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/fireworks.py +0 -0
  73. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/gemini.py +0 -0
  74. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/github_models.py +0 -0
  75. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/groq.py +0 -0
  76. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/image.py +0 -0
  77. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/mistral.py +0 -0
  78. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/nvidia.py +0 -0
  79. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/ollama.py +0 -0
  80. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/openai.py +0 -0
  81. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/openrouter.py +0 -0
  82. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/perplexity.py +0 -0
  83. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/qwen.py +0 -0
  84. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/speech.py +0 -0
  85. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/together.py +0 -0
  86. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/video.py +0 -0
  87. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/xai.py +0 -0
  88. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/providers/zai.py +0 -0
  89. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/py.typed +0 -0
  90. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/__init__.py +0 -0
  91. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/_shared.py +0 -0
  92. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/agent.py +0 -0
  93. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/chat.py +0 -0
  94. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/models.py +0 -0
  95. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/rag.py +0 -0
  96. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/quick/tools_agent.py +0 -0
  97. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/__init__.py +0 -0
  98. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/cleanup.py +0 -0
  99. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/identity.py +0 -0
  100. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/inputs.py +0 -0
  101. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/memory.py +0 -0
  102. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/plan_execute.py +0 -0
  103. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/reflection.py +0 -0
  104. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/resolve.py +0 -0
  105. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/rollback.py +0 -0
  106. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/search.py +0 -0
  107. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/skills.py +0 -0
  108. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/status.py +0 -0
  109. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/supervisor.py +0 -0
  110. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/text.py +0 -0
  111. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/recipes/tool_loop.py +0 -0
  112. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/redaction.py +0 -0
  113. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/relations.py +0 -0
  114. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/replay.py +0 -0
  115. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/request.py +0 -0
  116. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/runtime.py +0 -0
  117. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/scheduler.py +0 -0
  118. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/session.py +0 -0
  119. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/sources.py +0 -0
  120. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/streaming.py +0 -0
  121. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/structured.py +0 -0
  122. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/exceptions.py +0 -0
  123. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/fault.py +0 -0
  124. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/golden.py +0 -0
  125. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/mock.py +0 -0
  126. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/record.py +0 -0
  127. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/testing/registry.py +0 -0
  128. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tool_use.py +0 -0
  129. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tools.py +0 -0
  130. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/_otlp.py +0 -0
  131. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/langfuse.py +0 -0
  132. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/models.py +0 -0
  133. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/otlp.py +0 -0
  134. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/tracing/templates/sessions.html +0 -0
  135. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/triggers.py +0 -0
  136. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/verify.py +0 -0
  137. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/viz.py +0 -0
  138. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact/web.py +0 -0
  139. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact.egg-info/dependency_links.txt +0 -0
  140. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact.egg-info/entry_points.txt +0 -0
  141. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact.egg-info/requires.txt +0 -0
  142. {reactifact-0.11.2 → reactifact-0.12.0}/reactifact.egg-info/top_level.txt +0 -0
  143. {reactifact-0.11.2 → reactifact-0.12.0}/setup.cfg +0 -0
  144. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_absent_consume.py +0 -0
  145. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_adaptive.py +0 -0
  146. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_agent_tool.py +0 -0
  147. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_anthropic_provider.py +0 -0
  148. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_artifacts.py +0 -0
  149. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_audit.py +0 -0
  150. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_backbone.py +0 -0
  151. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_branching.py +0 -0
  152. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_budget.py +0 -0
  153. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_canonical_ports.py +0 -0
  154. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_chat_web.py +0 -0
  155. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_checkpoint.py +0 -0
  156. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_checkpoints_concurrency.py +0 -0
  157. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_cli.py +0 -0
  158. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_cli_scenario.py +0 -0
  159. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_commit_log.py +0 -0
  160. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_concurrency.py +0 -0
  161. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_consumes_produces.py +0 -0
  162. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_context_builder.py +0 -0
  163. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_correlated_consume.py +0 -0
  164. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_debounce.py +0 -0
  165. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_determinism.py +0 -0
  166. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_devops.py +0 -0
  167. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_effects.py +0 -0
  168. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_eval.py +0 -0
  169. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_fintech_audit.py +0 -0
  170. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_forklab.py +0 -0
  171. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_forklab_web.py +0 -0
  172. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_friendly_api.py +0 -0
  173. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_gemini_provider.py +0 -0
  174. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_golden.py +0 -0
  175. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_hitl.py +0 -0
  176. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_image_provider.py +0 -0
  177. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_incident_commander.py +0 -0
  178. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_invalidation.py +0 -0
  179. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_join_consume.py +0 -0
  180. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_knowledge.py +0 -0
  181. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_knowledge_web.py +0 -0
  182. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_llm_ladder.py +0 -0
  183. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_loop_bound_client.py +0 -0
  184. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_mcp.py +0 -0
  185. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_medic_lab.py +0 -0
  186. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_medic_lab_web.py +0 -0
  187. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_multisource.py +0 -0
  188. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_native_tool_use.py +0 -0
  189. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_openai_provider.py +0 -0
  190. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_patches.py +0 -0
  191. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_ports.py +0 -0
  192. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_produce_styles.py +0 -0
  193. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_produce_trigger.py +0 -0
  194. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_prompts.py +0 -0
  195. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_provider_auth.py +0 -0
  196. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_provider_retry.py +0 -0
  197. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_providers_integration.py +0 -0
  198. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_quick.py +0 -0
  199. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_reacts_to.py +0 -0
  200. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipe_plan_execute.py +0 -0
  201. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipe_reflection.py +0 -0
  202. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipe_supervisor.py +0 -0
  203. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipe_tool_loop.py +0 -0
  204. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipes.py +0 -0
  205. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipes_inputs.py +0 -0
  206. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipes_memory.py +0 -0
  207. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_recipes_skills.py +0 -0
  208. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_redaction.py +0 -0
  209. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_relation_graph.py +0 -0
  210. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_relations.py +0 -0
  211. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_repair.py +0 -0
  212. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_replay.py +0 -0
  213. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_repo_agent.py +0 -0
  214. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_request.py +0 -0
  215. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_research.py +0 -0
  216. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_resources.py +0 -0
  217. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_retry.py +0 -0
  218. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_runtime.py +0 -0
  219. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_runtime_errors.py +0 -0
  220. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_scheduler_semantics.py +0 -0
  221. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_sessions.py +0 -0
  222. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_sources.py +0 -0
  223. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_sources_search.py +0 -0
  224. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_sources_vector.py +0 -0
  225. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_speech_provider.py +0 -0
  226. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_starter_app.py +0 -0
  227. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_streaming.py +0 -0
  228. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_structured.py +0 -0
  229. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_support_copilot.py +0 -0
  230. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_testing_assertions.py +0 -0
  231. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_testing_mock.py +0 -0
  232. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_testing_registry.py +0 -0
  233. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_tools.py +0 -0
  234. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_trace_tags.py +0 -0
  235. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_tracing.py +0 -0
  236. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_vendor_factories.py +0 -0
  237. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_vendor_multimodal_factories.py +0 -0
  238. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_verify.py +0 -0
  239. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_video_provider.py +0 -0
  240. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_view.py +0 -0
  241. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_viz.py +0 -0
  242. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_web_source.py +0 -0
  243. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_workspace.py +0 -0
  244. {reactifact-0.11.2 → reactifact-0.12.0}/tests/test_workspace_with_sources.py +0 -0
  245. {reactifact-0.11.2 → reactifact-0.12.0}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.11.2
3
+ Version: 0.12.0
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.11.2"
7
+ version = "0.12.0"
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"
@@ -49,7 +49,7 @@ from .session import Session, SessionStore
49
49
  from .tools import FunctionTool, Tool, ToolOutput, tool
50
50
  from .triggers import Trigger
51
51
 
52
- __version__ = "0.11.2"
52
+ __version__ = "0.12.0"
53
53
 
54
54
  __all__ = [
55
55
  "Agent",
@@ -51,6 +51,7 @@ class RuntimeResources:
51
51
  context_builder: ContextBuilder | None = None,
52
52
  verification_threshold: float | None = None,
53
53
  redactor: Redactor | None = None,
54
+ trace_truncate: int | None = 1500,
54
55
  id_factory: IdFactory | None = None,
55
56
  **additional: Any,
56
57
  ):
@@ -67,6 +68,11 @@ class RuntimeResources:
67
68
  # persisted session. `None` (default) reproduces the pre-hook behavior.
68
69
  # See `reactifact.redaction`.
69
70
  self.redactor = redactor
71
+ # How many characters of an artifact's JSON (and an LLM response) are
72
+ # kept in a trace. `None` disables truncation. Larger values make
73
+ # configurable table columns / deep fields resolvable, at the cost of
74
+ # trace size.
75
+ self.trace_truncate = trace_truncate
70
76
  # Framework-wide pass/fail cutoff for `Verify` (verify.py): `None`
71
77
  # means "use Verify's own DEFAULT_THRESHOLD". A `Verify` instance's
72
78
  # own explicit `threshold=` still overrides this per agent.
@@ -20,7 +20,7 @@ from .assertions import (
20
20
  from .exceptions import AssertionFailure, ScenarioError, ScenarioSkip
21
21
  from .fault import ToolCallRecord, ToolCallRecorder, ToolFault
22
22
  from .golden import GoldenRun, assert_golden, capture, prompt_hashes, replay_resources
23
- from .lab import Scenario, ScenarioLab, ScenarioResult
23
+ from .lab import Scenario, ScenarioLab, ScenarioReport, ScenarioResult
24
24
  from .mock import ResourceFault
25
25
  from .record import Mode, mode_from_env
26
26
  from .registry import ScenarioCase, collect, scenario
@@ -38,6 +38,7 @@ __all__ = [
38
38
  "ScenarioCase",
39
39
  "ScenarioError",
40
40
  "ScenarioLab",
41
+ "ScenarioReport",
41
42
  "ScenarioResult",
42
43
  "ScenarioSkip",
43
44
  "ToolAssertions",
@@ -272,9 +272,17 @@ class LLMAssertions:
272
272
  def calls(self) -> int:
273
273
  return len(self._calls())
274
274
 
275
+ @property
276
+ def prompt_tokens(self) -> int:
277
+ return sum(c.prompt_tokens for c in self._calls())
278
+
279
+ @property
280
+ def completion_tokens(self) -> int:
281
+ return sum(c.completion_tokens for c in self._calls())
282
+
275
283
  @property
276
284
  def tokens(self) -> int:
277
- return sum(c.prompt_tokens + c.completion_tokens for c in self._calls())
285
+ return self.prompt_tokens + self.completion_tokens
278
286
 
279
287
  def max_calls(self, n: int) -> None:
280
288
  if self.calls > n:
@@ -24,7 +24,7 @@ from __future__ import annotations
24
24
 
25
25
  import asyncio
26
26
  from collections.abc import Callable, Sequence
27
- from dataclasses import dataclass
27
+ from dataclasses import dataclass, field
28
28
  from pathlib import Path
29
29
  from typing import Any, TypeVar
30
30
 
@@ -81,6 +81,97 @@ class _CapturingTracer(Tracer):
81
81
  self.trace = trace
82
82
 
83
83
 
84
+ @dataclass
85
+ class ScenarioReport:
86
+ """Aggregate cost/shape of a scenario run: what it took, in one object.
87
+
88
+ The reporting counterpart of the assertion groups — token totals, LLM/tool
89
+ counts, the agents that ran and the outcome, with `to_dict()` for a CI/log
90
+ artifact and `render()` for a readable multi-line summary. A single
91
+ `ScenarioResult.report` covers one turn; `Scenario.report` aggregates every
92
+ turn run so far.
93
+ """
94
+
95
+ turns: int = 0
96
+ spans: int = 0
97
+ agents: list[str] = field(default_factory=list)
98
+ tool_calls: int = 0
99
+ llm_calls: int = 0
100
+ prompt_tokens: int = 0
101
+ completion_tokens: int = 0
102
+ duration_ms: float = 0.0
103
+ errors: int = 0
104
+ outcomes: list[str] = field(default_factory=list)
105
+
106
+ @property
107
+ def total_tokens(self) -> int:
108
+ return self.prompt_tokens + self.completion_tokens
109
+
110
+ def to_dict(self) -> dict[str, Any]:
111
+ return {
112
+ "turns": self.turns,
113
+ "spans": self.spans,
114
+ "agents": list(self.agents),
115
+ "tool_calls": self.tool_calls,
116
+ "llm_calls": self.llm_calls,
117
+ "prompt_tokens": self.prompt_tokens,
118
+ "completion_tokens": self.completion_tokens,
119
+ "total_tokens": self.total_tokens,
120
+ "duration_ms": self.duration_ms,
121
+ "errors": self.errors,
122
+ "outcomes": list(self.outcomes),
123
+ }
124
+
125
+ def render(self) -> str:
126
+ rows = [
127
+ ("turns", str(self.turns)),
128
+ ("agents", ", ".join(self.agents) or "—"),
129
+ ("spans", str(self.spans)),
130
+ ("tool calls", str(self.tool_calls)),
131
+ ("llm calls", str(self.llm_calls)),
132
+ (
133
+ "tokens",
134
+ f"{self.prompt_tokens} in / {self.completion_tokens} out"
135
+ f" / {self.total_tokens} total",
136
+ ),
137
+ ("duration", f"{self.duration_ms:.1f} ms"),
138
+ ("errors", str(self.errors)),
139
+ ("outcomes", ", ".join(self.outcomes) or "—"),
140
+ ]
141
+ width = max(len(key) for key, _ in rows)
142
+ return "\n".join(
143
+ ["scenario report"] + [f" {key:<{width}} {value}" for key, value in rows]
144
+ )
145
+
146
+
147
+ def _scenario_report(
148
+ traces: list[RunTrace],
149
+ calls: list[ToolCallRecord],
150
+ *,
151
+ turns: int | None = None,
152
+ errors: int | None = None,
153
+ ) -> ScenarioReport:
154
+ """Builds a `ScenarioReport` off one or more run traces + recorded calls."""
155
+ spans = [span for trace in traces for span in trace.spans]
156
+ llm = [call for trace in traces for call in trace.llm_calls]
157
+ outcomes: list[str] = []
158
+ for trace in traces:
159
+ if trace.outcome and trace.outcome not in outcomes:
160
+ outcomes.append(trace.outcome)
161
+ return ScenarioReport(
162
+ turns=len(traces) if turns is None else turns,
163
+ spans=len(spans),
164
+ agents=sorted({span.agent for span in spans}),
165
+ tool_calls=len(calls),
166
+ llm_calls=len(llm),
167
+ prompt_tokens=sum(call.prompt_tokens for call in llm),
168
+ completion_tokens=sum(call.completion_tokens for call in llm),
169
+ duration_ms=round(sum(trace.duration_ms for trace in traces), 1),
170
+ errors=(sum(1 for span in spans if span.error) if errors is None else errors),
171
+ outcomes=outcomes,
172
+ )
173
+
174
+
84
175
  @dataclass
85
176
  class ScenarioResult:
86
177
  """Everything a scenario assertion needs, read off one `ScenarioLab.run()`.
@@ -98,6 +189,15 @@ class ScenarioResult:
98
189
  def artifacts(self, artifact_type: type[T]) -> ArtifactAssertions[T]:
99
190
  return ArtifactAssertions(self.context, artifact_type)
100
191
 
192
+ @property
193
+ def report(self) -> ScenarioReport:
194
+ """Token/latency/tool totals for this one turn (see `ScenarioReport`)."""
195
+ traces = [self.trace] if self.trace is not None else []
196
+ errors = None
197
+ if not traces and self.stats is not None:
198
+ errors = self.stats.errors
199
+ return _scenario_report(traces, self.calls, turns=1, errors=errors)
200
+
101
201
  @property
102
202
  def tools(self) -> ToolAssertions:
103
203
  return ToolAssertions(self.calls)
@@ -389,3 +489,8 @@ class Scenario:
389
489
  def errors(self) -> ErrorAssertions:
390
490
  """Isolated agent errors across every turn run so far."""
391
491
  return ErrorAssertions(_combined_trace(self.all_traces), None)
492
+
493
+ @property
494
+ def report(self) -> ScenarioReport:
495
+ """Aggregate `ScenarioReport` across every turn run so far."""
496
+ return _scenario_report(self.all_traces, self.all_calls)
@@ -5,6 +5,7 @@ Package: models (`models`), delivery (`tracer`), storage (`store`), UI
5
5
  but without `web`, so that importing the package does not pull in FastAPI.
6
6
  """
7
7
 
8
+ from .columns import TraceColumn
8
9
  from .langfuse import LangfuseTracer
9
10
  from .models import (
10
11
  AgentSpan,
@@ -41,6 +42,7 @@ __all__ = [
41
42
  "Tag",
42
43
  "TagAssignment",
43
44
  "TraceAnnotator",
45
+ "TraceColumn",
44
46
  "TraceReader",
45
47
  "TraceSink",
46
48
  "TraceStore",
@@ -0,0 +1,169 @@
1
+ """Configurable trace-table columns: pull a field out of a run's artifacts.
2
+
3
+ A `TraceColumn` names a column for the traces table and where its value comes
4
+ from: which agent's span, which artifact type, reads or writes, and a dotted
5
+ path into the artifact's data. A list step takes its first element unless an
6
+ explicit index is given, so `sources.title` means `sources[0].title`.
7
+
8
+ TraceColumn(label="Q", agent="start", type=Question, field="query")
9
+ TraceColumn(label="Answer", agent="normalize",
10
+ type=FinalResponse, field="answer")
11
+ TraceColumn(label="Top source", agent="search", type=Evidence,
12
+ field="sources.title")
13
+
14
+ Extraction runs over the artifact JSON persisted with each span. That JSON is
15
+ clipped by `RuntimeResources.trace_truncate` (default 1500 chars); a value cut
16
+ off mid-JSON cannot be parsed and falls back to the column's `default`.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ from collections.abc import Sequence
23
+ from dataclasses import dataclass
24
+ from dataclasses import field as dataclass_field
25
+ from typing import Any, Literal
26
+
27
+ from .models import ArtifactRef
28
+
29
+ #: Which side of a span to read the artifact from.
30
+ Direction = Literal["read", "write", "any"]
31
+
32
+ #: How far to look for the artifact: this run alone, or its whole session
33
+ #: (useful for a chat, where the question lives in the first run and the final
34
+ #: answer in a later one — every row then shows the session's Q/A).
35
+ Scope = Literal["run", "session"]
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class TraceColumn:
40
+ """One configurable column in the traces table.
41
+
42
+ `agent` / `type` narrow which artifacts are considered (both optional);
43
+ `direction` picks reads, writes or either; `index` selects among the
44
+ matches (default: the last one); `field` is a dotted path into that
45
+ artifact's JSON data (empty = the whole object). `scope="session"` looks
46
+ across every run of the row's session (chronological) instead of just this
47
+ run — so a chat's question (`index=0`) and final answer (`index=-1`) show on
48
+ every row. `default` is shown when nothing matches or the data is
49
+ unparseable.
50
+ """
51
+
52
+ label: str
53
+ field: str
54
+ agent: str | None = None
55
+ type: str | type | None = None
56
+ direction: Direction = "any"
57
+ index: int = -1
58
+ scope: Scope = "run"
59
+ default: str = "—"
60
+
61
+ def type_name(self) -> str | None:
62
+ if self.type is None:
63
+ return None
64
+ return getattr(self.type, "__name__", str(self.type))
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class SpanArtifacts:
69
+ """The minimum a column needs off a span: its agent + read/write refs."""
70
+
71
+ agent: str
72
+ reads: list[ArtifactRef] = dataclass_field(default_factory=list)
73
+ writes: list[ArtifactRef] = dataclass_field(default_factory=list)
74
+
75
+
76
+ def _candidates(
77
+ spans: Sequence[SpanArtifacts], column: TraceColumn
78
+ ) -> list[ArtifactRef]:
79
+ """Artifacts across `spans` matching the column's agent/type/direction."""
80
+ want = column.type_name()
81
+ found: list[ArtifactRef] = []
82
+ for span in spans:
83
+ if column.agent is not None and span.agent != column.agent:
84
+ continue
85
+ # `Runtime._collect_reads` records the triggering artifact first, then
86
+ # the other inputs — so the trigger (the newest artifact of the run) is
87
+ # first. Reverse reads so it comes *last*, making the default
88
+ # `index=-1` mean "most recent" across reads and writes alike.
89
+ reads = list(reversed(span.reads))
90
+ if column.direction == "write":
91
+ refs = span.writes
92
+ elif column.direction == "read":
93
+ refs = reads
94
+ else:
95
+ refs = [*reads, *span.writes]
96
+ found.extend(ref for ref in refs if want is None or ref.data_type == want)
97
+ return found
98
+
99
+
100
+ def _walk(value: Any, parts: Sequence[str]) -> Any:
101
+ """Descends `value` along `parts`; a list step takes element 0 by default."""
102
+ for part in parts:
103
+ if value is None:
104
+ return None
105
+ if isinstance(value, dict):
106
+ value = value.get(part)
107
+ elif isinstance(value, (list, tuple)):
108
+ if part.lstrip("-").isdigit():
109
+ index = int(part)
110
+ value = value[index] if -len(value) <= index < len(value) else None
111
+ elif value:
112
+ first = value[0]
113
+ value = first.get(part) if isinstance(first, dict) else None
114
+ else:
115
+ value = None
116
+ else:
117
+ return None
118
+ return value
119
+
120
+
121
+ def _display(value: Any) -> str | None:
122
+ if isinstance(value, (list, tuple)):
123
+ value = value[0] if value else None
124
+ if value is None:
125
+ return None
126
+ if isinstance(value, bool):
127
+ return "true" if value else "false"
128
+ if isinstance(value, dict):
129
+ return json.dumps(value, ensure_ascii=False, default=str)
130
+ return str(value)
131
+
132
+
133
+ def extract_columns(
134
+ spans: Sequence[SpanArtifacts],
135
+ columns: Sequence[TraceColumn],
136
+ *,
137
+ session_spans: Sequence[SpanArtifacts] | None = None,
138
+ ) -> dict[str, str]:
139
+ """Resolves every column to a display string for one run.
140
+
141
+ `spans` are the run's own spans; `session_spans` (when given) are every span
142
+ of the row's session in chronological order, used by `scope="session"`
143
+ columns. Best-effort by design (a dashboard convenience): a missing
144
+ artifact, an out-of-range index, or JSON clipped by the trace truncation
145
+ limit yields the column's `default` instead of raising.
146
+ """
147
+ out: dict[str, str] = {}
148
+ for column in columns:
149
+ source = session_spans if column.scope == "session" else spans
150
+ refs = _candidates(source if source is not None else spans, column)
151
+ ref: ArtifactRef | None = None
152
+ if refs:
153
+ try:
154
+ ref = refs[column.index]
155
+ except IndexError:
156
+ ref = None
157
+ value: Any = None
158
+ if ref is not None and ref.data:
159
+ try:
160
+ parsed = json.loads(ref.data)
161
+ except (ValueError, TypeError):
162
+ parsed = None
163
+ if isinstance(parsed, dict):
164
+ value = (
165
+ _walk(parsed, column.field.split(".")) if column.field else parsed
166
+ )
167
+ display = _display(value)
168
+ out[column.label] = display if display is not None else column.default
169
+ return out
@@ -18,9 +18,11 @@ be shared across requests.
18
18
 
19
19
  from __future__ import annotations
20
20
 
21
+ from collections.abc import Sequence
21
22
  from datetime import UTC, datetime
22
23
  from typing import Any
23
24
 
25
+ from .columns import SpanArtifacts, TraceColumn, extract_columns
24
26
  from .models import (
25
27
  AgentSpan,
26
28
  ArtifactRef,
@@ -238,6 +240,7 @@ class PostgresStore:
238
240
  q: str | None = None,
239
241
  sort: str = "started_at",
240
242
  order: str = "desc",
243
+ columns: Sequence[TraceColumn] | None = None,
241
244
  limit: int = 50,
242
245
  offset: int = 0,
243
246
  ) -> dict[str, Any]:
@@ -287,6 +290,69 @@ class PostgresStore:
287
290
  tags_by_run.setdefault(run_id, []).append(
288
291
  {"tag": tag, "note": note, "color": color}
289
292
  )
293
+ fields_by_run: dict[str, dict[str, str]] = {}
294
+ if run_ids and columns:
295
+ placeholders = ",".join("%s" for _ in run_ids)
296
+ await cur.execute(
297
+ "SELECT run_id, agent, reads, writes FROM spans "
298
+ f"WHERE run_id IN ({placeholders}) ORDER BY id",
299
+ tuple(run_ids),
300
+ )
301
+ spans_by_run: dict[str, list[SpanArtifacts]] = {}
302
+ for run_id, agent, reads, writes in await cur.fetchall():
303
+ spans_by_run.setdefault(run_id, []).append(
304
+ SpanArtifacts(
305
+ agent=agent,
306
+ reads=[ArtifactRef(**d) for d in reads],
307
+ writes=[ArtifactRef(**d) for d in writes],
308
+ )
309
+ )
310
+ needs_session = any(c.scope == "session" for c in columns)
311
+ session_order: dict[str, list[tuple[str, list[SpanArtifacts]]]] = {}
312
+ if needs_session:
313
+ sessions = sorted({r[1] for r in rows if r[1]})
314
+ for sid in sessions:
315
+ await cur.execute(
316
+ "SELECT id FROM runs WHERE session_id = %s "
317
+ "ORDER BY started_at ASC, id ASC",
318
+ (sid,),
319
+ )
320
+ ordered = [x[0] for x in await cur.fetchall()]
321
+ if not ordered:
322
+ continue
323
+ sph = ",".join("%s" for _ in ordered)
324
+ await cur.execute(
325
+ "SELECT run_id, agent, reads, writes FROM spans "
326
+ f"WHERE run_id IN ({sph}) ORDER BY id",
327
+ tuple(ordered),
328
+ )
329
+ by_run: dict[str, list[SpanArtifacts]] = {}
330
+ for run_id, agent, reads, writes in await cur.fetchall():
331
+ by_run.setdefault(run_id, []).append(
332
+ SpanArtifacts(
333
+ agent=agent,
334
+ reads=[ArtifactRef(**d) for d in reads],
335
+ writes=[ArtifactRef(**d) for d in writes],
336
+ )
337
+ )
338
+ session_order[sid] = [
339
+ (rid, by_run.get(rid, [])) for rid in ordered
340
+ ]
341
+ for row in rows:
342
+ session_spans: list[SpanArtifacts] | None = None
343
+ if needs_session:
344
+ ordered = session_order.get(row[1])
345
+ if ordered is not None:
346
+ session_spans = []
347
+ for rid, spans in ordered:
348
+ session_spans.extend(spans)
349
+ if rid == row[0]:
350
+ break
351
+ fields_by_run[row[0]] = extract_columns(
352
+ spans_by_run.get(row[0], []),
353
+ columns,
354
+ session_spans=session_spans,
355
+ )
290
356
  finally:
291
357
  await conn.close()
292
358
 
@@ -301,6 +367,7 @@ class PostgresStore:
301
367
  "completion_tokens": r[6],
302
368
  "spans": r[7],
303
369
  "tags": tags_by_run.get(r[0], []),
370
+ "fields": fields_by_run.get(r[0], {}),
304
371
  }
305
372
  for r in rows
306
373
  ]
@@ -374,6 +441,63 @@ class PostgresStore:
374
441
  annotations=annotations,
375
442
  )
376
443
 
444
+ async def field_values(
445
+ self, run_id: str, columns: Sequence[TraceColumn]
446
+ ) -> dict[str, str]:
447
+ if not columns:
448
+ return {}
449
+ await self._ensure_schema()
450
+ conn = await self._psycopg.AsyncConnection.connect(self.dsn)
451
+ try:
452
+ async with conn.cursor() as cur:
453
+ await cur.execute(
454
+ "SELECT session_id FROM runs WHERE id = %s", (run_id,)
455
+ )
456
+ row = await cur.fetchone()
457
+ if row is None:
458
+ return {}
459
+ session_id = row[0]
460
+
461
+ async def spans_of(
462
+ run_ids: list[str],
463
+ ) -> dict[str, list[SpanArtifacts]]:
464
+ placeholders = ",".join("%s" for _ in run_ids)
465
+ await cur.execute(
466
+ "SELECT run_id, agent, reads, writes FROM spans "
467
+ f"WHERE run_id IN ({placeholders}) ORDER BY id",
468
+ tuple(run_ids),
469
+ )
470
+ out: dict[str, list[SpanArtifacts]] = {}
471
+ for rid, agent, reads, writes in await cur.fetchall():
472
+ out.setdefault(rid, []).append(
473
+ SpanArtifacts(
474
+ agent=agent,
475
+ reads=[ArtifactRef(**d) for d in reads],
476
+ writes=[ArtifactRef(**d) for d in writes],
477
+ )
478
+ )
479
+ return out
480
+
481
+ run_spans = (await spans_of([run_id])).get(run_id, [])
482
+ session_spans: list[SpanArtifacts] | None = None
483
+ if session_id and any(c.scope == "session" for c in columns):
484
+ await cur.execute(
485
+ "SELECT id FROM runs WHERE session_id = %s "
486
+ "ORDER BY started_at ASC, id ASC",
487
+ (session_id,),
488
+ )
489
+ ordered = [r[0] for r in await cur.fetchall()]
490
+ if ordered:
491
+ by_run = await spans_of(ordered)
492
+ session_spans = []
493
+ for rid in ordered:
494
+ session_spans.extend(by_run.get(rid, []))
495
+ if rid == run_id:
496
+ break
497
+ finally:
498
+ await conn.close()
499
+ return extract_columns(run_spans, columns, session_spans=session_spans)
500
+
377
501
  async def sessions(
378
502
  self,
379
503
  *,