reactifact 0.16.0__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.0/reactifact.egg-info → reactifact-0.17.0}/PKG-INFO +1 -1
  2. {reactifact-0.16.0 → reactifact-0.17.0}/pyproject.toml +1 -1
  3. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/__init__.py +1 -1
  4. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/resources.py +24 -5
  5. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/runtime.py +1 -1
  6. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/__init__.py +4 -0
  7. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/models.py +43 -0
  8. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/postgres.py +195 -55
  9. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/store.py +139 -33
  10. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/templates/app.css +20 -0
  11. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/templates/sessions.html +10 -2
  12. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/templates/ui_run.html +166 -50
  13. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/tracer.py +77 -3
  14. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/web.py +97 -35
  15. {reactifact-0.16.0 → reactifact-0.17.0/reactifact.egg-info}/PKG-INFO +1 -1
  16. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact.egg-info/SOURCES.txt +1 -0
  17. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_public_api.py +2 -0
  18. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_trace_columns.py +7 -3
  19. reactifact-0.17.0/tests/test_trace_limits.py +131 -0
  20. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_trace_ui.py +12 -1
  21. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_tracing.py +13 -4
  22. {reactifact-0.16.0 → reactifact-0.17.0}/LICENSE +0 -0
  23. {reactifact-0.16.0 → reactifact-0.17.0}/README.md +0 -0
  24. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/__main__.py +0 -0
  25. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/_extras.py +0 -0
  26. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/_httpx.py +0 -0
  27. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/_persistence.py +0 -0
  28. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/a2a/__init__.py +0 -0
  29. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/a2a/client.py +0 -0
  30. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/a2a/models.py +0 -0
  31. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/a2a/remote.py +0 -0
  32. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/a2a/server.py +0 -0
  33. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/agent_tool.py +0 -0
  34. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/agents.py +0 -0
  35. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/artifacts.py +0 -0
  36. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/audit.py +0 -0
  37. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/authz.py +0 -0
  38. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/branching.py +0 -0
  39. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/budget.py +0 -0
  40. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cache.py +0 -0
  41. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/chat.py +0 -0
  42. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/checkpoints.py +0 -0
  43. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/__init__.py +0 -0
  44. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/branch.py +0 -0
  45. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/common.py +0 -0
  46. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/context.py +0 -0
  47. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/eval.py +0 -0
  48. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/graph.py +0 -0
  49. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/replay.py +0 -0
  50. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/scenario.py +0 -0
  51. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/skills.py +0 -0
  52. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/cli/trace.py +0 -0
  53. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/commit.py +0 -0
  54. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/commit_log.py +0 -0
  55. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/consume.py +0 -0
  56. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/context.py +0 -0
  57. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/context_builder.py +0 -0
  58. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/effects.py +0 -0
  59. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/__init__.py +0 -0
  60. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/dataset.py +0 -0
  61. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/evaluators.py +0 -0
  62. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/judge.py +0 -0
  63. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/online.py +0 -0
  64. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/runner.py +0 -0
  65. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/eval/scoring.py +0 -0
  66. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/events.py +0 -0
  67. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/guardrails.py +0 -0
  68. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/interrupt.py +0 -0
  69. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/llm_agent.py +0 -0
  70. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/logging.py +0 -0
  71. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/mcp/__init__.py +0 -0
  72. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/mcp/client.py +0 -0
  73. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/mcp/oauth.py +0 -0
  74. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/mcp/server.py +0 -0
  75. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/metrics.py +0 -0
  76. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/native_tool_use.py +0 -0
  77. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/operations.py +0 -0
  78. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/patches.py +0 -0
  79. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/pricing.py +0 -0
  80. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/produce.py +0 -0
  81. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/prompts.py +0 -0
  82. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/__init__.py +0 -0
  83. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/_retry.py +0 -0
  84. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/anthropic.py +0 -0
  85. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/azure.py +0 -0
  86. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/cerebras.py +0 -0
  87. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/chat.py +0 -0
  88. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/contracts.py +0 -0
  89. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/deepseek.py +0 -0
  90. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/fake.py +0 -0
  91. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/fireworks.py +0 -0
  92. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/gemini.py +0 -0
  93. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/github_models.py +0 -0
  94. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/groq.py +0 -0
  95. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/image.py +0 -0
  96. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/mistral.py +0 -0
  97. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/nvidia.py +0 -0
  98. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/ollama.py +0 -0
  99. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/openai.py +0 -0
  100. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/openrouter.py +0 -0
  101. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/perplexity.py +0 -0
  102. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/qwen.py +0 -0
  103. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/speech.py +0 -0
  104. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/together.py +0 -0
  105. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/video.py +0 -0
  106. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/xai.py +0 -0
  107. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/providers/zai.py +0 -0
  108. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/py.typed +0 -0
  109. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/__init__.py +0 -0
  110. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/_shared.py +0 -0
  111. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/agent.py +0 -0
  112. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/chat.py +0 -0
  113. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/models.py +0 -0
  114. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/rag.py +0 -0
  115. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quick/tools_agent.py +0 -0
  116. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/quota.py +0 -0
  117. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/__init__.py +0 -0
  118. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/cleanup.py +0 -0
  119. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/conversation.py +0 -0
  120. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/identity.py +0 -0
  121. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/inputs.py +0 -0
  122. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/memory.py +0 -0
  123. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/plan_execute.py +0 -0
  124. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/reflection.py +0 -0
  125. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/resolve.py +0 -0
  126. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/rollback.py +0 -0
  127. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/search.py +0 -0
  128. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/skills.py +0 -0
  129. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/status.py +0 -0
  130. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/supervisor.py +0 -0
  131. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/text.py +0 -0
  132. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/recipes/tool_loop.py +0 -0
  133. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/redaction.py +0 -0
  134. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/relations.py +0 -0
  135. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/replay.py +0 -0
  136. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/request.py +0 -0
  137. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/routing.py +0 -0
  138. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/scheduler.py +0 -0
  139. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/session.py +0 -0
  140. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact/SKILL.md +0 -0
  141. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  142. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  143. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  144. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  145. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  146. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  147. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  148. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  149. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  150. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  151. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  152. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  153. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  154. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  155. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  156. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/sources.py +0 -0
  157. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/streaming.py +0 -0
  158. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/structured.py +0 -0
  159. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/__init__.py +0 -0
  160. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/assertions.py +0 -0
  161. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/exceptions.py +0 -0
  162. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/fault.py +0 -0
  163. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/golden.py +0 -0
  164. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/lab.py +0 -0
  165. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/mock.py +0 -0
  166. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/pytest_plugin.py +0 -0
  167. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/record.py +0 -0
  168. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/testing/registry.py +0 -0
  169. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tool_use.py +0 -0
  170. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tools.py +0 -0
  171. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/_otlp.py +0 -0
  172. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/columns.py +0 -0
  173. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/langfuse.py +0 -0
  174. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/otlp.py +0 -0
  175. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/tracing/templates/ui.html +0 -0
  176. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/triggers.py +0 -0
  177. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/types.py +0 -0
  178. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/verify.py +0 -0
  179. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/viz.py +0 -0
  180. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact/web.py +0 -0
  181. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact.egg-info/dependency_links.txt +0 -0
  182. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact.egg-info/entry_points.txt +0 -0
  183. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact.egg-info/requires.txt +0 -0
  184. {reactifact-0.16.0 → reactifact-0.17.0}/reactifact.egg-info/top_level.txt +0 -0
  185. {reactifact-0.16.0 → reactifact-0.17.0}/setup.cfg +0 -0
  186. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_a2a.py +0 -0
  187. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_a2a_example.py +0 -0
  188. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_a2a_interop.py +0 -0
  189. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_absent_consume.py +0 -0
  190. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_adaptive.py +0 -0
  191. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_agent_tool.py +0 -0
  192. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_anthropic_provider.py +0 -0
  193. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_artifacts.py +0 -0
  194. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_audit.py +0 -0
  195. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_authz.py +0 -0
  196. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_backbone.py +0 -0
  197. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_branching.py +0 -0
  198. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_budget.py +0 -0
  199. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_cache.py +0 -0
  200. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_canonical_ports.py +0 -0
  201. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_chat_example.py +0 -0
  202. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_chat_memory.py +0 -0
  203. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_chat_outbox.py +0 -0
  204. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_chat_web.py +0 -0
  205. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_checkpoint.py +0 -0
  206. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_checkpoints_concurrency.py +0 -0
  207. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_cli.py +0 -0
  208. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_cli_scenario.py +0 -0
  209. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_commit_log.py +0 -0
  210. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_compaction.py +0 -0
  211. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_concurrency.py +0 -0
  212. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_consumes_produces.py +0 -0
  213. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_context_builder.py +0 -0
  214. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_correlated_consume.py +0 -0
  215. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_debounce.py +0 -0
  216. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_determinism.py +0 -0
  217. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_devops.py +0 -0
  218. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_devops_online.py +0 -0
  219. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_devops_web.py +0 -0
  220. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_effects.py +0 -0
  221. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_eval.py +0 -0
  222. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_eval_dataset.py +0 -0
  223. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_eval_judge.py +0 -0
  224. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_eval_online.py +0 -0
  225. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_eval_trajectory.py +0 -0
  226. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_fintech_audit.py +0 -0
  227. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_forklab.py +0 -0
  228. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_forklab_web.py +0 -0
  229. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_friendly_api.py +0 -0
  230. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_gemini_provider.py +0 -0
  231. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_golden.py +0 -0
  232. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_guardrails.py +0 -0
  233. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_hitl.py +0 -0
  234. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_image_provider.py +0 -0
  235. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_incident_commander.py +0 -0
  236. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_invalidation.py +0 -0
  237. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_join_consume.py +0 -0
  238. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_knowledge.py +0 -0
  239. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_knowledge_web.py +0 -0
  240. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_llm_ladder.py +0 -0
  241. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_logging.py +0 -0
  242. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_loop_bound_client.py +0 -0
  243. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_mcp.py +0 -0
  244. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_medic_lab.py +0 -0
  245. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_medic_lab_web.py +0 -0
  246. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_metrics.py +0 -0
  247. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_multisource.py +0 -0
  248. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_native_tool_use.py +0 -0
  249. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_openai_provider.py +0 -0
  250. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_outbox.py +0 -0
  251. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_patches.py +0 -0
  252. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_pending_actions.py +0 -0
  253. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_ports.py +0 -0
  254. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_produce_styles.py +0 -0
  255. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_produce_trigger.py +0 -0
  256. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_prompts.py +0 -0
  257. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_provider_auth.py +0 -0
  258. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_provider_retry.py +0 -0
  259. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_providers_integration.py +0 -0
  260. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_quick.py +0 -0
  261. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_quota.py +0 -0
  262. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_reacts_to.py +0 -0
  263. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipe_conversation.py +0 -0
  264. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipe_plan_execute.py +0 -0
  265. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipe_reflection.py +0 -0
  266. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipe_supervisor.py +0 -0
  267. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipe_tool_loop.py +0 -0
  268. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipes.py +0 -0
  269. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipes_inputs.py +0 -0
  270. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipes_memory.py +0 -0
  271. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_recipes_skills.py +0 -0
  272. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_redaction.py +0 -0
  273. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_relation_graph.py +0 -0
  274. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_relations.py +0 -0
  275. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_repair.py +0 -0
  276. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_repair_web.py +0 -0
  277. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_replay.py +0 -0
  278. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_repo_agent.py +0 -0
  279. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_request.py +0 -0
  280. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_research.py +0 -0
  281. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_resources.py +0 -0
  282. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_retry.py +0 -0
  283. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_routing.py +0 -0
  284. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_runtime.py +0 -0
  285. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_runtime_errors.py +0 -0
  286. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_scheduler_semantics.py +0 -0
  287. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_session_append.py +0 -0
  288. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_sessions.py +0 -0
  289. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_shutdown.py +0 -0
  290. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_skills.py +0 -0
  291. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_sources.py +0 -0
  292. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_sources_search.py +0 -0
  293. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_sources_vector.py +0 -0
  294. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_speech_provider.py +0 -0
  295. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_starter_app.py +0 -0
  296. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_streaming.py +0 -0
  297. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_structured.py +0 -0
  298. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_support_copilot.py +0 -0
  299. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_assertions.py +0 -0
  300. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_golden.py +0 -0
  301. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_lab.py +0 -0
  302. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_mock.py +0 -0
  303. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_provenance.py +0 -0
  304. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_pytest_plugin.py +0 -0
  305. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_testing_registry.py +0 -0
  306. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_token_budget.py +0 -0
  307. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_tools.py +0 -0
  308. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_trace_tags.py +0 -0
  309. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_type_registry.py +0 -0
  310. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_vendor_factories.py +0 -0
  311. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_vendor_multimodal_factories.py +0 -0
  312. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_verify.py +0 -0
  313. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_video_provider.py +0 -0
  314. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_view.py +0 -0
  315. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_viz.py +0 -0
  316. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_web_source.py +0 -0
  317. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_workspace.py +0 -0
  318. {reactifact-0.16.0 → reactifact-0.17.0}/tests/test_workspace_with_sources.py +0 -0
  319. {reactifact-0.16.0 → 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.0
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.0"
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.0"
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
@@ -9,6 +9,7 @@ from ..tools import ToolRun
9
9
  from .columns import TraceColumn
10
10
  from .langfuse import LangfuseTracer
11
11
  from .models import (
12
+ AgentContract,
12
13
  AgentSpan,
13
14
  ArtifactRef,
14
15
  LLMCall,
@@ -17,6 +18,7 @@ from .models import (
17
18
  RunTrace,
18
19
  Tag,
19
20
  TagAssignment,
21
+ TraceLimits,
20
22
  )
21
23
  from .otlp import OTLPTracer
22
24
  from .postgres import PostgresStore
@@ -30,6 +32,7 @@ from .store import (
30
32
  from .tracer import CompositeTracer, RecordingLLM, RunTracer, Tracer
31
33
 
32
34
  __all__ = [
35
+ "AgentContract",
33
36
  "AgentSpan",
34
37
  "ArtifactRef",
35
38
  "CompositeTracer",
@@ -46,6 +49,7 @@ __all__ = [
46
49
  "TagAssignment",
47
50
  "TraceAnnotator",
48
51
  "TraceColumn",
52
+ "TraceLimits",
49
53
  "TraceReader",
50
54
  "TraceSink",
51
55
  "TraceStore",
@@ -90,6 +90,20 @@ class TagAssignment(BaseModel):
90
90
  updated_at: datetime | None = None
91
91
 
92
92
 
93
+ class AgentContract(BaseModel):
94
+ """The agent's declared contract, recorded on each span for the UI.
95
+
96
+ `consumes`/`produces`/`tools` are the *declared* names (not only what ran),
97
+ so the dashboard can show a consume/produce that never fired as a muted
98
+ chip instead of hiding it.
99
+ """
100
+
101
+ consumes: list[str] = Field(default_factory=list)
102
+ produces: list[str] = Field(default_factory=list)
103
+ tools: list[str] = Field(default_factory=list)
104
+ priority: int = 0
105
+
106
+
93
107
  class ProduceRun(BaseModel):
94
108
  """One produce execution within an agent span (§54).
95
109
 
@@ -104,6 +118,23 @@ class ProduceRun(BaseModel):
104
118
  latency_ms: float = 0.0
105
119
 
106
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
+
107
138
  class AgentSpan(BaseModel):
108
139
  """A single agent execution within a run (§54)."""
109
140
 
@@ -113,6 +144,10 @@ class AgentSpan(BaseModel):
113
144
  #: ``Consume``) — e.g. ``DraftAnswer`` vs ``FinalResponse`` for an agent that
114
145
  #: consumes both. ``event_type`` alone (created/updated) can't tell them apart.
115
146
  event_artifact_type: str = ""
147
+ #: The concrete artifact whose change triggered this span.
148
+ event_artifact_id: str = ""
149
+ #: The agent's declared consumes/produces/tools (for the UI contract view).
150
+ contract: AgentContract = Field(default_factory=AgentContract)
116
151
  #: The produces that ran for this event, in execution order.
117
152
  produces: list[ProduceRun] = Field(default_factory=list)
118
153
  #: Tool executions that happened during this agent run (name/latency/status).
@@ -135,6 +170,14 @@ class RunTrace(BaseModel):
135
170
  duration_ms: float = 0.0
136
171
  outcome: str = ""
137
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
138
181
  #: Reviewer annotations attached *after* the run (UI review workflow, §54).
139
182
  #: The runtime always leaves this empty; `TraceStore.get` populates it.
140
183
  annotations: list[TagAssignment] = Field(default_factory=list)
@@ -25,6 +25,7 @@ from typing import Any
25
25
  from ..tools import ToolRun
26
26
  from .columns import SpanArtifacts, TraceColumn, extract_columns
27
27
  from .models import (
28
+ AgentContract,
28
29
  AgentSpan,
29
30
  ArtifactRef,
30
31
  LLMCall,
@@ -39,11 +40,21 @@ from .store import _SORT_COLUMNS, _like
39
40
  class PostgresStore:
40
41
  """Postgres trace sink with async write + read. Requires the `pg` extra."""
41
42
 
42
- 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
+ ):
43
51
  from .._extras import require_extra
44
52
 
45
53
  self._psycopg = require_extra("PostgresStore", "psycopg", "pg")
46
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
47
58
  self._schema_ready = False
48
59
 
49
60
  async def _ensure_schema(self) -> None:
@@ -61,7 +72,8 @@ class PostgresStore:
61
72
  duration_ms REAL NOT NULL,
62
73
  outcome TEXT NOT NULL,
63
74
  prompt_tokens INTEGER NOT NULL DEFAULT 0,
64
- completion_tokens INTEGER NOT NULL DEFAULT 0
75
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
76
+ truncated INTEGER NOT NULL DEFAULT 0
65
77
  )
66
78
  """
67
79
  )
@@ -81,7 +93,9 @@ class PostgresStore:
81
93
  relations JSONB NOT NULL DEFAULT '[]',
82
94
  llm_calls JSONB NOT NULL DEFAULT '[]',
83
95
  produces JSONB NOT NULL DEFAULT '[]',
84
- tools JSONB NOT NULL DEFAULT '[]'
96
+ tools JSONB NOT NULL DEFAULT '[]',
97
+ event_artifact_id TEXT NOT NULL DEFAULT '',
98
+ contract JSONB NOT NULL DEFAULT '{}'
85
99
  )
86
100
  """
87
101
  )
@@ -124,6 +138,10 @@ class PostgresStore:
124
138
  "ALTER TABLE runs ADD COLUMN IF NOT EXISTS "
125
139
  "completion_tokens INTEGER NOT NULL DEFAULT 0"
126
140
  )
141
+ await cur.execute(
142
+ "ALTER TABLE runs ADD COLUMN IF NOT EXISTS "
143
+ "truncated INTEGER NOT NULL DEFAULT 0"
144
+ )
127
145
  await cur.execute(
128
146
  "ALTER TABLE spans ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ"
129
147
  )
@@ -139,6 +157,14 @@ class PostgresStore:
139
157
  "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
140
158
  "tools JSONB NOT NULL DEFAULT '[]'"
141
159
  )
160
+ await cur.execute(
161
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
162
+ "event_artifact_id TEXT NOT NULL DEFAULT ''"
163
+ )
164
+ await cur.execute(
165
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
166
+ "contract JSONB NOT NULL DEFAULT '{}'"
167
+ )
142
168
  await conn.commit()
143
169
  finally:
144
170
  await conn.close()
@@ -159,8 +185,8 @@ class PostgresStore:
159
185
  async with conn.cursor() as cur:
160
186
  await cur.execute(
161
187
  "INSERT INTO runs (id, session_id, started_at, duration_ms, "
162
- "outcome, prompt_tokens, completion_tokens) "
163
- "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)",
164
190
  (
165
191
  trace.id,
166
192
  trace.session_id,
@@ -169,6 +195,7 @@ class PostgresStore:
169
195
  trace.outcome,
170
196
  prompt_tokens,
171
197
  completion_tokens,
198
+ 1 if trace.trace_truncated else 0,
172
199
  ),
173
200
  )
174
201
  for span in trace.spans:
@@ -178,8 +205,8 @@ class PostgresStore:
178
205
  await cur.execute(
179
206
  "INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
180
207
  "latency_ms, error, started_at, reads, writes, relations, llm_calls, "
181
- "produces, tools) "
182
- "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
208
+ "produces, tools, event_artifact_id, contract) "
209
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
183
210
  (
184
211
  trace.id,
185
212
  span.agent,
@@ -206,11 +233,91 @@ class PostgresStore:
206
233
  psycopg.types.json.Jsonb(
207
234
  [tr.model_dump(mode="json") for tr in span.tools]
208
235
  ),
236
+ span.event_artifact_id,
237
+ psycopg.types.json.Jsonb(
238
+ span.contract.model_dump(mode="json")
239
+ ),
209
240
  ),
210
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)
275
+ await conn.commit()
276
+ finally:
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
+ )
211
317
  await conn.commit()
212
318
  finally:
213
319
  await conn.close()
320
+ return len(ids)
214
321
 
215
322
  def _where(
216
323
  self,
@@ -317,21 +424,32 @@ class PostgresStore:
317
424
  )
318
425
  fields_by_run: dict[str, dict[str, str]] = {}
319
426
  if run_ids and columns:
320
- placeholders = ",".join("%s" for _ in run_ids)
321
- await cur.execute(
322
- "SELECT run_id, agent, reads, writes FROM spans "
323
- f"WHERE run_id IN ({placeholders}) ORDER BY id",
324
- tuple(run_ids),
325
- )
326
- spans_by_run: dict[str, list[SpanArtifacts]] = {}
327
- for run_id, agent, reads, writes in await cur.fetchall():
328
- spans_by_run.setdefault(run_id, []).append(
329
- SpanArtifacts(
330
- agent=agent,
331
- reads=[ArtifactRef(**d) for d in reads],
332
- 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"
333
436
  )
334
- )
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)
335
453
  needs_session = any(c.scope == "session" for c in columns)
336
454
  session_order: dict[str, list[tuple[str, list[SpanArtifacts]]]] = {}
337
455
  if needs_session:
@@ -345,21 +463,10 @@ class PostgresStore:
345
463
  ordered = [x[0] for x in await cur.fetchall()]
346
464
  if not ordered:
347
465
  continue
348
- sph = ",".join("%s" for _ in ordered)
349
- await cur.execute(
350
- "SELECT run_id, agent, reads, writes FROM spans "
351
- f"WHERE run_id IN ({sph}) ORDER BY id",
352
- tuple(ordered),
353
- )
354
- by_run: dict[str, list[SpanArtifacts]] = {}
355
- for run_id, agent, reads, writes in await cur.fetchall():
356
- by_run.setdefault(run_id, []).append(
357
- SpanArtifacts(
358
- agent=agent,
359
- reads=[ArtifactRef(**d) for d in reads],
360
- writes=[ArtifactRef(**d) for d in writes],
361
- )
362
- )
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)
363
470
  session_order[sid] = [
364
471
  (rid, by_run.get(rid, [])) for rid in ordered
365
472
  ]
@@ -393,18 +500,29 @@ class PostgresStore:
393
500
  "spans": r[7],
394
501
  "tags": tags_by_run.get(r[0], []),
395
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
+ ),
396
508
  }
397
509
  for r in rows
398
510
  ]
399
511
  return {"items": items, "total": total, "stats": stats}
400
512
 
401
- 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:
402
520
  await self._ensure_schema()
403
521
  conn = await self._psycopg.AsyncConnection.connect(self.dsn)
404
522
  try:
405
523
  async with conn.cursor() as cur:
406
524
  await cur.execute(
407
- "SELECT id, session_id, started_at, duration_ms, outcome "
525
+ "SELECT id, session_id, started_at, duration_ms, outcome, truncated "
408
526
  "FROM runs WHERE id = %s",
409
527
  (trace_id,),
410
528
  )
@@ -412,11 +530,20 @@ class PostgresStore:
412
530
  if row is None:
413
531
  return None
414
532
  await cur.execute(
415
- "SELECT agent, event_type, event_artifact_type, latency_ms, error, "
416
- "started_at, reads, writes, relations, llm_calls, produces, tools "
417
- "FROM spans WHERE run_id = %s ORDER BY id",
418
- (trace_id,),
533
+ "SELECT COUNT(*) FROM spans WHERE run_id = %s", (trace_id,)
419
534
  )
535
+ span_total = (await cur.fetchone())[0]
536
+ span_sql = (
537
+ "SELECT agent, event_type, event_artifact_type, latency_ms, error, "
538
+ "started_at, reads, writes, relations, llm_calls, produces, tools, "
539
+ "event_artifact_id, contract "
540
+ "FROM spans WHERE run_id = %s ORDER BY id"
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)
420
547
  span_rows = await cur.fetchall()
421
548
  await cur.execute(
422
549
  "SELECT rt.tag, rt.note, COALESCE(t.color, ''), "
@@ -443,6 +570,8 @@ class PostgresStore:
443
570
  llm_calls=[LLMCall(**d) for d in r[9]],
444
571
  produces=[ProduceRun(**d) for d in r[10]],
445
572
  tools=[ToolRun(**d) for d in r[11]],
573
+ event_artifact_id=r[12],
574
+ contract=AgentContract(**r[13]),
446
575
  )
447
576
  for r in span_rows
448
577
  ]
@@ -466,6 +595,10 @@ class PostgresStore:
466
595
  duration_ms=row[3],
467
596
  outcome=row[4],
468
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]),
469
602
  annotations=annotations,
470
603
  )
471
604
 
@@ -489,21 +622,25 @@ class PostgresStore:
489
622
  async def spans_of(
490
623
  run_ids: list[str],
491
624
  ) -> dict[str, list[SpanArtifacts]]:
492
- placeholders = ",".join("%s" for _ in run_ids)
493
- await cur.execute(
494
- "SELECT run_id, agent, reads, writes FROM spans "
495
- f"WHERE run_id IN ({placeholders}) ORDER BY id",
496
- tuple(run_ids),
497
- )
498
625
  out: dict[str, list[SpanArtifacts]] = {}
499
- for rid, agent, reads, writes in await cur.fetchall():
500
- out.setdefault(rid, []).append(
501
- SpanArtifacts(
502
- agent=agent,
503
- reads=[ArtifactRef(**d) for d in reads],
504
- writes=[ArtifactRef(**d) for d in writes],
505
- )
626
+ for rid in run_ids:
627
+ sql = (
628
+ "SELECT agent, reads, writes FROM spans "
629
+ "WHERE run_id = %s ORDER BY id"
506
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
+ )
507
644
  return out
508
645
 
509
646
  run_spans = (await spans_of([run_id])).get(run_id, [])
@@ -516,6 +653,9 @@ class PostgresStore:
516
653
  )
517
654
  ordered = [r[0] for r in await cur.fetchall()]
518
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:]
519
659
  by_run = await spans_of(ordered)
520
660
  session_spans = []
521
661
  for rid in ordered: