reactifact 0.16.1__tar.gz → 0.17.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 (319) hide show
  1. {reactifact-0.16.1/reactifact.egg-info → reactifact-0.17.0}/PKG-INFO +1 -1
  2. {reactifact-0.16.1 → reactifact-0.17.0}/pyproject.toml +1 -1
  3. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/__init__.py +1 -1
  4. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/resources.py +24 -5
  5. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/runtime.py +1 -1
  6. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/__init__.py +2 -0
  7. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/models.py +25 -0
  8. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/postgres.py +172 -50
  9. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/store.py +119 -29
  10. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/templates/sessions.html +10 -2
  11. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/templates/ui_run.html +56 -12
  12. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/tracer.py +47 -2
  13. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/web.py +97 -35
  14. {reactifact-0.16.1 → reactifact-0.17.0/reactifact.egg-info}/PKG-INFO +1 -1
  15. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact.egg-info/SOURCES.txt +1 -0
  16. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_public_api.py +1 -0
  17. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_trace_columns.py +7 -3
  18. reactifact-0.17.0/tests/test_trace_limits.py +131 -0
  19. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_tracing.py +6 -4
  20. {reactifact-0.16.1 → reactifact-0.17.0}/LICENSE +0 -0
  21. {reactifact-0.16.1 → reactifact-0.17.0}/README.md +0 -0
  22. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/__main__.py +0 -0
  23. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/_extras.py +0 -0
  24. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/_httpx.py +0 -0
  25. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/_persistence.py +0 -0
  26. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/a2a/__init__.py +0 -0
  27. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/a2a/client.py +0 -0
  28. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/a2a/models.py +0 -0
  29. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/a2a/remote.py +0 -0
  30. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/a2a/server.py +0 -0
  31. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/agent_tool.py +0 -0
  32. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/agents.py +0 -0
  33. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/artifacts.py +0 -0
  34. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/audit.py +0 -0
  35. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/authz.py +0 -0
  36. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/branching.py +0 -0
  37. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/budget.py +0 -0
  38. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cache.py +0 -0
  39. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/chat.py +0 -0
  40. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/checkpoints.py +0 -0
  41. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/__init__.py +0 -0
  42. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/branch.py +0 -0
  43. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/common.py +0 -0
  44. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/context.py +0 -0
  45. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/eval.py +0 -0
  46. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/graph.py +0 -0
  47. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/replay.py +0 -0
  48. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/scenario.py +0 -0
  49. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/skills.py +0 -0
  50. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/cli/trace.py +0 -0
  51. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/commit.py +0 -0
  52. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/commit_log.py +0 -0
  53. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/consume.py +0 -0
  54. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/context.py +0 -0
  55. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/context_builder.py +0 -0
  56. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/effects.py +0 -0
  57. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/__init__.py +0 -0
  58. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/dataset.py +0 -0
  59. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/evaluators.py +0 -0
  60. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/judge.py +0 -0
  61. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/online.py +0 -0
  62. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/runner.py +0 -0
  63. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/eval/scoring.py +0 -0
  64. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/events.py +0 -0
  65. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/guardrails.py +0 -0
  66. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/interrupt.py +0 -0
  67. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/llm_agent.py +0 -0
  68. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/logging.py +0 -0
  69. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/mcp/__init__.py +0 -0
  70. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/mcp/client.py +0 -0
  71. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/mcp/oauth.py +0 -0
  72. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/mcp/server.py +0 -0
  73. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/metrics.py +0 -0
  74. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/native_tool_use.py +0 -0
  75. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/operations.py +0 -0
  76. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/patches.py +0 -0
  77. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/pricing.py +0 -0
  78. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/produce.py +0 -0
  79. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/prompts.py +0 -0
  80. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/__init__.py +0 -0
  81. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/_retry.py +0 -0
  82. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/anthropic.py +0 -0
  83. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/azure.py +0 -0
  84. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/cerebras.py +0 -0
  85. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/chat.py +0 -0
  86. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/contracts.py +0 -0
  87. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/deepseek.py +0 -0
  88. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/fake.py +0 -0
  89. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/fireworks.py +0 -0
  90. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/gemini.py +0 -0
  91. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/github_models.py +0 -0
  92. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/groq.py +0 -0
  93. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/image.py +0 -0
  94. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/mistral.py +0 -0
  95. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/nvidia.py +0 -0
  96. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/ollama.py +0 -0
  97. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/openai.py +0 -0
  98. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/openrouter.py +0 -0
  99. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/perplexity.py +0 -0
  100. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/qwen.py +0 -0
  101. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/speech.py +0 -0
  102. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/together.py +0 -0
  103. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/video.py +0 -0
  104. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/xai.py +0 -0
  105. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/providers/zai.py +0 -0
  106. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/py.typed +0 -0
  107. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/__init__.py +0 -0
  108. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/_shared.py +0 -0
  109. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/agent.py +0 -0
  110. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/chat.py +0 -0
  111. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/models.py +0 -0
  112. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/rag.py +0 -0
  113. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quick/tools_agent.py +0 -0
  114. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/quota.py +0 -0
  115. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/__init__.py +0 -0
  116. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/cleanup.py +0 -0
  117. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/conversation.py +0 -0
  118. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/identity.py +0 -0
  119. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/inputs.py +0 -0
  120. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/memory.py +0 -0
  121. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/plan_execute.py +0 -0
  122. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/reflection.py +0 -0
  123. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/resolve.py +0 -0
  124. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/rollback.py +0 -0
  125. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/search.py +0 -0
  126. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/skills.py +0 -0
  127. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/status.py +0 -0
  128. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/supervisor.py +0 -0
  129. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/text.py +0 -0
  130. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/recipes/tool_loop.py +0 -0
  131. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/redaction.py +0 -0
  132. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/relations.py +0 -0
  133. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/replay.py +0 -0
  134. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/request.py +0 -0
  135. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/routing.py +0 -0
  136. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/scheduler.py +0 -0
  137. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/session.py +0 -0
  138. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact/SKILL.md +0 -0
  139. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  140. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  141. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  142. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  143. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  144. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  145. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  146. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  147. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  148. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  149. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  150. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  151. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  152. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  153. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  154. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/sources.py +0 -0
  155. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/streaming.py +0 -0
  156. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/structured.py +0 -0
  157. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/__init__.py +0 -0
  158. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/assertions.py +0 -0
  159. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/exceptions.py +0 -0
  160. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/fault.py +0 -0
  161. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/golden.py +0 -0
  162. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/lab.py +0 -0
  163. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/mock.py +0 -0
  164. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/pytest_plugin.py +0 -0
  165. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/record.py +0 -0
  166. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/testing/registry.py +0 -0
  167. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tool_use.py +0 -0
  168. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tools.py +0 -0
  169. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/_otlp.py +0 -0
  170. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/columns.py +0 -0
  171. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/langfuse.py +0 -0
  172. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/otlp.py +0 -0
  173. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/templates/app.css +0 -0
  174. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/tracing/templates/ui.html +0 -0
  175. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/triggers.py +0 -0
  176. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/types.py +0 -0
  177. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/verify.py +0 -0
  178. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/viz.py +0 -0
  179. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact/web.py +0 -0
  180. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact.egg-info/dependency_links.txt +0 -0
  181. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact.egg-info/entry_points.txt +0 -0
  182. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact.egg-info/requires.txt +0 -0
  183. {reactifact-0.16.1 → reactifact-0.17.0}/reactifact.egg-info/top_level.txt +0 -0
  184. {reactifact-0.16.1 → reactifact-0.17.0}/setup.cfg +0 -0
  185. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_a2a.py +0 -0
  186. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_a2a_example.py +0 -0
  187. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_a2a_interop.py +0 -0
  188. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_absent_consume.py +0 -0
  189. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_adaptive.py +0 -0
  190. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_agent_tool.py +0 -0
  191. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_anthropic_provider.py +0 -0
  192. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_artifacts.py +0 -0
  193. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_audit.py +0 -0
  194. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_authz.py +0 -0
  195. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_backbone.py +0 -0
  196. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_branching.py +0 -0
  197. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_budget.py +0 -0
  198. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_cache.py +0 -0
  199. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_canonical_ports.py +0 -0
  200. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_chat_example.py +0 -0
  201. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_chat_memory.py +0 -0
  202. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_chat_outbox.py +0 -0
  203. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_chat_web.py +0 -0
  204. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_checkpoint.py +0 -0
  205. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_checkpoints_concurrency.py +0 -0
  206. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_cli.py +0 -0
  207. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_cli_scenario.py +0 -0
  208. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_commit_log.py +0 -0
  209. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_compaction.py +0 -0
  210. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_concurrency.py +0 -0
  211. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_consumes_produces.py +0 -0
  212. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_context_builder.py +0 -0
  213. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_correlated_consume.py +0 -0
  214. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_debounce.py +0 -0
  215. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_determinism.py +0 -0
  216. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_devops.py +0 -0
  217. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_devops_online.py +0 -0
  218. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_devops_web.py +0 -0
  219. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_effects.py +0 -0
  220. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_eval.py +0 -0
  221. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_eval_dataset.py +0 -0
  222. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_eval_judge.py +0 -0
  223. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_eval_online.py +0 -0
  224. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_eval_trajectory.py +0 -0
  225. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_fintech_audit.py +0 -0
  226. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_forklab.py +0 -0
  227. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_forklab_web.py +0 -0
  228. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_friendly_api.py +0 -0
  229. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_gemini_provider.py +0 -0
  230. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_golden.py +0 -0
  231. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_guardrails.py +0 -0
  232. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_hitl.py +0 -0
  233. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_image_provider.py +0 -0
  234. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_incident_commander.py +0 -0
  235. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_invalidation.py +0 -0
  236. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_join_consume.py +0 -0
  237. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_knowledge.py +0 -0
  238. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_knowledge_web.py +0 -0
  239. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_llm_ladder.py +0 -0
  240. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_logging.py +0 -0
  241. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_loop_bound_client.py +0 -0
  242. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_mcp.py +0 -0
  243. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_medic_lab.py +0 -0
  244. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_medic_lab_web.py +0 -0
  245. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_metrics.py +0 -0
  246. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_multisource.py +0 -0
  247. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_native_tool_use.py +0 -0
  248. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_openai_provider.py +0 -0
  249. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_outbox.py +0 -0
  250. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_patches.py +0 -0
  251. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_pending_actions.py +0 -0
  252. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_ports.py +0 -0
  253. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_produce_styles.py +0 -0
  254. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_produce_trigger.py +0 -0
  255. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_prompts.py +0 -0
  256. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_provider_auth.py +0 -0
  257. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_provider_retry.py +0 -0
  258. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_providers_integration.py +0 -0
  259. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_quick.py +0 -0
  260. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_quota.py +0 -0
  261. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_reacts_to.py +0 -0
  262. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipe_conversation.py +0 -0
  263. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipe_plan_execute.py +0 -0
  264. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipe_reflection.py +0 -0
  265. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipe_supervisor.py +0 -0
  266. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipe_tool_loop.py +0 -0
  267. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipes.py +0 -0
  268. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipes_inputs.py +0 -0
  269. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipes_memory.py +0 -0
  270. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_recipes_skills.py +0 -0
  271. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_redaction.py +0 -0
  272. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_relation_graph.py +0 -0
  273. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_relations.py +0 -0
  274. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_repair.py +0 -0
  275. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_repair_web.py +0 -0
  276. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_replay.py +0 -0
  277. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_repo_agent.py +0 -0
  278. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_request.py +0 -0
  279. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_research.py +0 -0
  280. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_resources.py +0 -0
  281. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_retry.py +0 -0
  282. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_routing.py +0 -0
  283. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_runtime.py +0 -0
  284. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_runtime_errors.py +0 -0
  285. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_scheduler_semantics.py +0 -0
  286. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_session_append.py +0 -0
  287. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_sessions.py +0 -0
  288. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_shutdown.py +0 -0
  289. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_skills.py +0 -0
  290. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_sources.py +0 -0
  291. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_sources_search.py +0 -0
  292. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_sources_vector.py +0 -0
  293. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_speech_provider.py +0 -0
  294. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_starter_app.py +0 -0
  295. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_streaming.py +0 -0
  296. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_structured.py +0 -0
  297. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_support_copilot.py +0 -0
  298. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_assertions.py +0 -0
  299. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_golden.py +0 -0
  300. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_lab.py +0 -0
  301. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_mock.py +0 -0
  302. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_provenance.py +0 -0
  303. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_pytest_plugin.py +0 -0
  304. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_testing_registry.py +0 -0
  305. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_token_budget.py +0 -0
  306. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_tools.py +0 -0
  307. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_trace_tags.py +0 -0
  308. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_trace_ui.py +0 -0
  309. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_type_registry.py +0 -0
  310. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_vendor_factories.py +0 -0
  311. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_vendor_multimodal_factories.py +0 -0
  312. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_verify.py +0 -0
  313. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_video_provider.py +0 -0
  314. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_view.py +0 -0
  315. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_viz.py +0 -0
  316. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_web_source.py +0 -0
  317. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_workspace.py +0 -0
  318. {reactifact-0.16.1 → reactifact-0.17.0}/tests/test_workspace_with_sources.py +0 -0
  319. {reactifact-0.16.1 → reactifact-0.17.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.16.1
3
+ Version: 0.17.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.16.1"
7
+ version = "0.17.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"
@@ -51,7 +51,7 @@ from .tools import FunctionTool, Tool, ToolError, ToolOutput, tool
51
51
  from .triggers import Trigger
52
52
  from .types import register as register_type
53
53
 
54
- __version__ = "0.16.1"
54
+ __version__ = "0.17.0"
55
55
 
56
56
  __all__ = [
57
57
  "Agent",
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Awaitable, Callable
5
6
  from contextlib import AbstractAsyncContextManager
6
7
  from typing import (
@@ -27,6 +28,15 @@ if TYPE_CHECKING:
27
28
 
28
29
  T = TypeVar("T")
29
30
 
31
+
32
+ class _UnsetTraceTruncate:
33
+ """Sentinel so an explicit `trace_truncate=` can be told apart from the default."""
34
+
35
+ __slots__ = ()
36
+
37
+
38
+ _UNSET_TRACE_TRUNCATE = _UnsetTraceTruncate()
39
+
30
40
  #: Mints an artifact id from its model/type name — injectable for deterministic
31
41
  #: runs (`reactifact.replay.counter_ids`). `None` keeps the uuid default.
32
42
  IdFactory = Callable[[str], str]
@@ -85,7 +95,7 @@ class RuntimeResources:
85
95
  context_builder: ContextBuilder | None = None,
86
96
  verification_threshold: float | None = None,
87
97
  redactor: Redactor | None = None,
88
- trace_truncate: int | None = 1500,
98
+ trace_truncate: int | None | _UnsetTraceTruncate = _UNSET_TRACE_TRUNCATE,
89
99
  record_tool_args: bool = False,
90
100
  id_factory: IdFactory | None = None,
91
101
  pricer: Pricer | None = None,
@@ -136,10 +146,19 @@ class RuntimeResources:
136
146
  # `redactor` when set). Off by default because args may carry secrets.
137
147
  self.record_tool_args = record_tool_args
138
148
  # How many characters of an artifact's JSON (and an LLM response) are
139
- # kept in a trace. `None` disables truncation. Larger values make
140
- # configurable table columns / deep fields resolvable, at the cost of
141
- # trace size.
142
- self.trace_truncate = trace_truncate
149
+ # kept in a trace. `None` disables truncation. Deprecated: configure
150
+ # `Tracer(limits=TraceLimits(max_data_chars=...))` instead; this stays
151
+ # as a fallback for older code.
152
+ if isinstance(trace_truncate, _UnsetTraceTruncate):
153
+ self.trace_truncate: int | None = 1500
154
+ else:
155
+ warnings.warn(
156
+ "RuntimeResources(trace_truncate=...) is deprecated; use "
157
+ "Tracer(limits=TraceLimits(max_data_chars=...)) instead.",
158
+ DeprecationWarning,
159
+ stacklevel=2,
160
+ )
161
+ self.trace_truncate = trace_truncate
143
162
  # Framework-wide pass/fail cutoff for `Verify` (verify.py): `None`
144
163
  # means "use Verify's own DEFAULT_THRESHOLD". A `Verify` instance's
145
164
  # own explicit `threshold=` still overrides this per agent.
@@ -853,7 +853,7 @@ class Runtime:
853
853
  tool_runs_token = begin_tool_runs(
854
854
  record_args=self.context.resources.record_tool_args,
855
855
  redactor=self.context.resources.redactor,
856
- truncate=self.context.resources.trace_truncate,
856
+ truncate=self._trace.data_char_limit,
857
857
  )
858
858
  slot: Effects | None = None
859
859
  patch: Patch | None = None
@@ -18,6 +18,7 @@ from .models import (
18
18
  RunTrace,
19
19
  Tag,
20
20
  TagAssignment,
21
+ TraceLimits,
21
22
  )
22
23
  from .otlp import OTLPTracer
23
24
  from .postgres import PostgresStore
@@ -48,6 +49,7 @@ __all__ = [
48
49
  "TagAssignment",
49
50
  "TraceAnnotator",
50
51
  "TraceColumn",
52
+ "TraceLimits",
51
53
  "TraceReader",
52
54
  "TraceSink",
53
55
  "TraceStore",
@@ -118,6 +118,23 @@ class ProduceRun(BaseModel):
118
118
  latency_ms: float = 0.0
119
119
 
120
120
 
121
+ class TraceLimits(BaseModel):
122
+ """Write-time limits for one recorded run.
123
+
124
+ Configure on the tracer (`Tracer(store=..., limits=TraceLimits(...))`),
125
+ not on `RuntimeResources`: these are recording policy, independent of the
126
+ app's providers/sources. When a cap is hit, extra data is dropped and the
127
+ trace is marked `trace_truncated` instead of growing without bound.
128
+ """
129
+
130
+ max_spans: int | None = 5000
131
+ max_refs_per_span: int | None = 50
132
+ max_llm_calls_per_span: int | None = 20
133
+ #: Per-artifact/response text cap. `None` = use
134
+ #: `RuntimeResources.trace_truncate` (the legacy setting).
135
+ max_data_chars: int | None = None
136
+
137
+
121
138
  class AgentSpan(BaseModel):
122
139
  """A single agent execution within a run (§54)."""
123
140
 
@@ -153,6 +170,14 @@ class RunTrace(BaseModel):
153
170
  duration_ms: float = 0.0
154
171
  outcome: str = ""
155
172
  spans: list[AgentSpan] = Field(default_factory=list)
173
+ #: Total spans stored for this run (even when `spans` is a page).
174
+ span_total: int = 0
175
+ #: Offset of `spans` within the stored run (for pagination).
176
+ span_offset: int = 0
177
+ #: Whether the store has more spans beyond this page.
178
+ has_more_spans: bool = False
179
+ #: Whether the run was truncated while it was recorded (write-time cap).
180
+ trace_truncated: bool = False
156
181
  #: Reviewer annotations attached *after* the run (UI review workflow, §54).
157
182
  #: The runtime always leaves this empty; `TraceStore.get` populates it.
158
183
  annotations: list[TagAssignment] = Field(default_factory=list)
@@ -40,11 +40,21 @@ from .store import _SORT_COLUMNS, _like
40
40
  class PostgresStore:
41
41
  """Postgres trace sink with async write + read. Requires the `pg` extra."""
42
42
 
43
- def __init__(self, dsn: str):
43
+ def __init__(
44
+ self,
45
+ dsn: str,
46
+ *,
47
+ max_runs: int | None = 200,
48
+ column_scan_limit: int | None = 1000,
49
+ max_session_runs: int = 50,
50
+ ):
44
51
  from .._extras import require_extra
45
52
 
46
53
  self._psycopg = require_extra("PostgresStore", "psycopg", "pg")
47
54
  self.dsn = dsn
55
+ self.max_runs = max_runs
56
+ self.column_scan_limit = column_scan_limit
57
+ self.max_session_runs = max_session_runs
48
58
  self._schema_ready = False
49
59
 
50
60
  async def _ensure_schema(self) -> None:
@@ -62,7 +72,8 @@ class PostgresStore:
62
72
  duration_ms REAL NOT NULL,
63
73
  outcome TEXT NOT NULL,
64
74
  prompt_tokens INTEGER NOT NULL DEFAULT 0,
65
- completion_tokens INTEGER NOT NULL DEFAULT 0
75
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
76
+ truncated INTEGER NOT NULL DEFAULT 0
66
77
  )
67
78
  """
68
79
  )
@@ -127,6 +138,10 @@ class PostgresStore:
127
138
  "ALTER TABLE runs ADD COLUMN IF NOT EXISTS "
128
139
  "completion_tokens INTEGER NOT NULL DEFAULT 0"
129
140
  )
141
+ await cur.execute(
142
+ "ALTER TABLE runs ADD COLUMN IF NOT EXISTS "
143
+ "truncated INTEGER NOT NULL DEFAULT 0"
144
+ )
130
145
  await cur.execute(
131
146
  "ALTER TABLE spans ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ"
132
147
  )
@@ -170,8 +185,8 @@ class PostgresStore:
170
185
  async with conn.cursor() as cur:
171
186
  await cur.execute(
172
187
  "INSERT INTO runs (id, session_id, started_at, duration_ms, "
173
- "outcome, prompt_tokens, completion_tokens) "
174
- "VALUES (%s, %s, %s, %s, %s, %s, %s)",
188
+ "outcome, prompt_tokens, completion_tokens, truncated) "
189
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
175
190
  (
176
191
  trace.id,
177
192
  trace.session_id,
@@ -180,6 +195,7 @@ class PostgresStore:
180
195
  trace.outcome,
181
196
  prompt_tokens,
182
197
  completion_tokens,
198
+ 1 if trace.trace_truncated else 0,
183
199
  ),
184
200
  )
185
201
  for span in trace.spans:
@@ -223,9 +239,85 @@ class PostgresStore:
223
239
  ),
224
240
  ),
225
241
  )
242
+ if self.max_runs is not None:
243
+ await self._prune_cur(cur, self.max_runs)
244
+ await conn.commit()
245
+ finally:
246
+ await conn.close()
247
+
248
+ async def _prune_cur(self, cur: Any, keep: int) -> int:
249
+ await cur.execute(
250
+ "SELECT id FROM runs ORDER BY started_at DESC, id DESC OFFSET %s",
251
+ (keep,),
252
+ )
253
+ ids = [r[0] for r in await cur.fetchall()]
254
+ if not ids:
255
+ return 0
256
+ placeholders = ",".join("%s" for _ in ids)
257
+ await cur.execute(
258
+ f"DELETE FROM spans WHERE run_id IN ({placeholders})", tuple(ids)
259
+ )
260
+ await cur.execute(
261
+ f"DELETE FROM run_tags WHERE run_id IN ({placeholders})", tuple(ids)
262
+ )
263
+ await cur.execute(f"DELETE FROM runs WHERE id IN ({placeholders})", tuple(ids))
264
+ return len(ids)
265
+
266
+ async def prune(self, keep: int | None = None) -> int:
267
+ keep = self.max_runs if keep is None else keep
268
+ if keep is None:
269
+ return 0
270
+ await self._ensure_schema()
271
+ conn = await self._psycopg.AsyncConnection.connect(self.dsn)
272
+ try:
273
+ async with conn.cursor() as cur:
274
+ removed = await self._prune_cur(cur, keep)
226
275
  await conn.commit()
227
276
  finally:
228
277
  await conn.close()
278
+ return removed
279
+
280
+ async def delete_run(self, trace_id: str) -> bool:
281
+ await self._ensure_schema()
282
+ conn = await self._psycopg.AsyncConnection.connect(self.dsn)
283
+ try:
284
+ async with conn.cursor() as cur:
285
+ await cur.execute("SELECT 1 FROM runs WHERE id = %s", (trace_id,))
286
+ if await cur.fetchone() is None:
287
+ return False
288
+ await cur.execute("DELETE FROM spans WHERE run_id = %s", (trace_id,))
289
+ await cur.execute("DELETE FROM run_tags WHERE run_id = %s", (trace_id,))
290
+ await cur.execute("DELETE FROM runs WHERE id = %s", (trace_id,))
291
+ await conn.commit()
292
+ finally:
293
+ await conn.close()
294
+ return True
295
+
296
+ async def delete_session(self, session_id: str) -> int:
297
+ await self._ensure_schema()
298
+ conn = await self._psycopg.AsyncConnection.connect(self.dsn)
299
+ try:
300
+ async with conn.cursor() as cur:
301
+ await cur.execute(
302
+ "SELECT id FROM runs WHERE session_id = %s", (session_id,)
303
+ )
304
+ ids = [r[0] for r in await cur.fetchall()]
305
+ if not ids:
306
+ return 0
307
+ placeholders = ",".join("%s" for _ in ids)
308
+ await cur.execute(
309
+ f"DELETE FROM spans WHERE run_id IN ({placeholders})", tuple(ids)
310
+ )
311
+ await cur.execute(
312
+ f"DELETE FROM run_tags WHERE run_id IN ({placeholders})", tuple(ids)
313
+ )
314
+ await cur.execute(
315
+ f"DELETE FROM runs WHERE id IN ({placeholders})", tuple(ids)
316
+ )
317
+ await conn.commit()
318
+ finally:
319
+ await conn.close()
320
+ return len(ids)
229
321
 
230
322
  def _where(
231
323
  self,
@@ -332,21 +424,32 @@ class PostgresStore:
332
424
  )
333
425
  fields_by_run: dict[str, dict[str, str]] = {}
334
426
  if run_ids and columns:
335
- placeholders = ",".join("%s" for _ in run_ids)
336
- await cur.execute(
337
- "SELECT run_id, agent, reads, writes FROM spans "
338
- f"WHERE run_id IN ({placeholders}) ORDER BY id",
339
- tuple(run_ids),
340
- )
341
- spans_by_run: dict[str, list[SpanArtifacts]] = {}
342
- for run_id, agent, reads, writes in await cur.fetchall():
343
- spans_by_run.setdefault(run_id, []).append(
344
- SpanArtifacts(
345
- agent=agent,
346
- reads=[ArtifactRef(**d) for d in reads],
347
- writes=[ArtifactRef(**d) for d in writes],
427
+
428
+ async def spans_of(
429
+ ids: list[str],
430
+ ) -> dict[str, list[SpanArtifacts]]:
431
+ out: dict[str, list[SpanArtifacts]] = {}
432
+ for rid in ids:
433
+ sql = (
434
+ "SELECT agent, reads, writes FROM spans "
435
+ "WHERE run_id = %s ORDER BY id"
348
436
  )
349
- )
437
+ args: tuple[Any, ...] = (rid,)
438
+ if self.column_scan_limit is not None:
439
+ sql += " LIMIT %s"
440
+ args = (rid, self.column_scan_limit)
441
+ await cur.execute(sql, args)
442
+ for agent, reads, writes in await cur.fetchall():
443
+ out.setdefault(rid, []).append(
444
+ SpanArtifacts(
445
+ agent=agent,
446
+ reads=[ArtifactRef(**d) for d in reads],
447
+ writes=[ArtifactRef(**d) for d in writes],
448
+ )
449
+ )
450
+ return out
451
+
452
+ spans_by_run = await spans_of(run_ids)
350
453
  needs_session = any(c.scope == "session" for c in columns)
351
454
  session_order: dict[str, list[tuple[str, list[SpanArtifacts]]]] = {}
352
455
  if needs_session:
@@ -360,21 +463,10 @@ class PostgresStore:
360
463
  ordered = [x[0] for x in await cur.fetchall()]
361
464
  if not ordered:
362
465
  continue
363
- sph = ",".join("%s" for _ in ordered)
364
- await cur.execute(
365
- "SELECT run_id, agent, reads, writes FROM spans "
366
- f"WHERE run_id IN ({sph}) ORDER BY id",
367
- tuple(ordered),
368
- )
369
- by_run: dict[str, list[SpanArtifacts]] = {}
370
- for run_id, agent, reads, writes in await cur.fetchall():
371
- by_run.setdefault(run_id, []).append(
372
- SpanArtifacts(
373
- agent=agent,
374
- reads=[ArtifactRef(**d) for d in reads],
375
- writes=[ArtifactRef(**d) for d in writes],
376
- )
377
- )
466
+ if len(ordered) > self.max_session_runs:
467
+ half = max(1, self.max_session_runs // 2)
468
+ ordered = ordered[:half] + ordered[-half:]
469
+ by_run = await spans_of(ordered)
378
470
  session_order[sid] = [
379
471
  (rid, by_run.get(rid, [])) for rid in ordered
380
472
  ]
@@ -408,18 +500,29 @@ class PostgresStore:
408
500
  "spans": r[7],
409
501
  "tags": tags_by_run.get(r[0], []),
410
502
  "fields": fields_by_run.get(r[0], {}),
503
+ "fields_truncated": bool(
504
+ columns
505
+ and self.column_scan_limit is not None
506
+ and r[7] > self.column_scan_limit
507
+ ),
411
508
  }
412
509
  for r in rows
413
510
  ]
414
511
  return {"items": items, "total": total, "stats": stats}
415
512
 
416
- async def get(self, trace_id: str) -> RunTrace | None:
513
+ async def get(
514
+ self,
515
+ trace_id: str,
516
+ *,
517
+ span_limit: int | None = 500,
518
+ span_offset: int = 0,
519
+ ) -> RunTrace | None:
417
520
  await self._ensure_schema()
418
521
  conn = await self._psycopg.AsyncConnection.connect(self.dsn)
419
522
  try:
420
523
  async with conn.cursor() as cur:
421
524
  await cur.execute(
422
- "SELECT id, session_id, started_at, duration_ms, outcome "
525
+ "SELECT id, session_id, started_at, duration_ms, outcome, truncated "
423
526
  "FROM runs WHERE id = %s",
424
527
  (trace_id,),
425
528
  )
@@ -427,12 +530,20 @@ class PostgresStore:
427
530
  if row is None:
428
531
  return None
429
532
  await cur.execute(
533
+ "SELECT COUNT(*) FROM spans WHERE run_id = %s", (trace_id,)
534
+ )
535
+ span_total = (await cur.fetchone())[0]
536
+ span_sql = (
430
537
  "SELECT agent, event_type, event_artifact_type, latency_ms, error, "
431
538
  "started_at, reads, writes, relations, llm_calls, produces, tools, "
432
539
  "event_artifact_id, contract "
433
- "FROM spans WHERE run_id = %s ORDER BY id",
434
- (trace_id,),
540
+ "FROM spans WHERE run_id = %s ORDER BY id"
435
541
  )
542
+ span_args: tuple[Any, ...] = (trace_id,)
543
+ if span_limit is not None:
544
+ span_sql += " LIMIT %s OFFSET %s"
545
+ span_args = (trace_id, span_limit, span_offset)
546
+ await cur.execute(span_sql, span_args)
436
547
  span_rows = await cur.fetchall()
437
548
  await cur.execute(
438
549
  "SELECT rt.tag, rt.note, COALESCE(t.color, ''), "
@@ -484,6 +595,10 @@ class PostgresStore:
484
595
  duration_ms=row[3],
485
596
  outcome=row[4],
486
597
  spans=spans,
598
+ span_total=span_total,
599
+ span_offset=span_offset,
600
+ has_more_spans=span_offset + len(spans) < span_total,
601
+ trace_truncated=bool(row[5]),
487
602
  annotations=annotations,
488
603
  )
489
604
 
@@ -507,21 +622,25 @@ class PostgresStore:
507
622
  async def spans_of(
508
623
  run_ids: list[str],
509
624
  ) -> dict[str, list[SpanArtifacts]]:
510
- placeholders = ",".join("%s" for _ in run_ids)
511
- await cur.execute(
512
- "SELECT run_id, agent, reads, writes FROM spans "
513
- f"WHERE run_id IN ({placeholders}) ORDER BY id",
514
- tuple(run_ids),
515
- )
516
625
  out: dict[str, list[SpanArtifacts]] = {}
517
- for rid, agent, reads, writes in await cur.fetchall():
518
- out.setdefault(rid, []).append(
519
- SpanArtifacts(
520
- agent=agent,
521
- reads=[ArtifactRef(**d) for d in reads],
522
- writes=[ArtifactRef(**d) for d in writes],
523
- )
626
+ for rid in run_ids:
627
+ sql = (
628
+ "SELECT agent, reads, writes FROM spans "
629
+ "WHERE run_id = %s ORDER BY id"
524
630
  )
631
+ args: tuple[Any, ...] = (rid,)
632
+ if self.column_scan_limit is not None:
633
+ sql += " LIMIT %s"
634
+ args = (rid, self.column_scan_limit)
635
+ await cur.execute(sql, args)
636
+ for agent, reads, writes in await cur.fetchall():
637
+ out.setdefault(rid, []).append(
638
+ SpanArtifacts(
639
+ agent=agent,
640
+ reads=[ArtifactRef(**d) for d in reads],
641
+ writes=[ArtifactRef(**d) for d in writes],
642
+ )
643
+ )
525
644
  return out
526
645
 
527
646
  run_spans = (await spans_of([run_id])).get(run_id, [])
@@ -534,6 +653,9 @@ class PostgresStore:
534
653
  )
535
654
  ordered = [r[0] for r in await cur.fetchall()]
536
655
  if ordered:
656
+ if len(ordered) > self.max_session_runs:
657
+ half = max(1, self.max_session_runs // 2)
658
+ ordered = ordered[:half] + ordered[-half:]
537
659
  by_run = await spans_of(ordered)
538
660
  session_spans = []
539
661
  for rid in ordered: