reactifact 0.16.0__tar.gz → 0.16.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. {reactifact-0.16.0/reactifact.egg-info → reactifact-0.16.1}/PKG-INFO +1 -1
  2. {reactifact-0.16.0 → reactifact-0.16.1}/pyproject.toml +1 -1
  3. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/__init__.py +1 -1
  4. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/__init__.py +2 -0
  5. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/models.py +18 -0
  6. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/postgres.py +22 -4
  7. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/store.py +20 -4
  8. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/templates/app.css +20 -0
  9. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/templates/ui_run.html +109 -37
  10. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/tracer.py +30 -1
  11. {reactifact-0.16.0 → reactifact-0.16.1/reactifact.egg-info}/PKG-INFO +1 -1
  12. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_public_api.py +1 -0
  13. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_trace_ui.py +12 -1
  14. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_tracing.py +7 -0
  15. {reactifact-0.16.0 → reactifact-0.16.1}/LICENSE +0 -0
  16. {reactifact-0.16.0 → reactifact-0.16.1}/README.md +0 -0
  17. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/__main__.py +0 -0
  18. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/_extras.py +0 -0
  19. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/_httpx.py +0 -0
  20. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/_persistence.py +0 -0
  21. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/a2a/__init__.py +0 -0
  22. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/a2a/client.py +0 -0
  23. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/a2a/models.py +0 -0
  24. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/a2a/remote.py +0 -0
  25. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/a2a/server.py +0 -0
  26. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/agent_tool.py +0 -0
  27. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/agents.py +0 -0
  28. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/artifacts.py +0 -0
  29. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/audit.py +0 -0
  30. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/authz.py +0 -0
  31. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/branching.py +0 -0
  32. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/budget.py +0 -0
  33. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cache.py +0 -0
  34. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/chat.py +0 -0
  35. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/checkpoints.py +0 -0
  36. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/__init__.py +0 -0
  37. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/branch.py +0 -0
  38. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/common.py +0 -0
  39. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/context.py +0 -0
  40. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/eval.py +0 -0
  41. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/graph.py +0 -0
  42. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/replay.py +0 -0
  43. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/scenario.py +0 -0
  44. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/skills.py +0 -0
  45. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/cli/trace.py +0 -0
  46. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/commit.py +0 -0
  47. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/commit_log.py +0 -0
  48. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/consume.py +0 -0
  49. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/context.py +0 -0
  50. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/context_builder.py +0 -0
  51. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/effects.py +0 -0
  52. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/__init__.py +0 -0
  53. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/dataset.py +0 -0
  54. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/evaluators.py +0 -0
  55. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/judge.py +0 -0
  56. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/online.py +0 -0
  57. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/runner.py +0 -0
  58. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/eval/scoring.py +0 -0
  59. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/events.py +0 -0
  60. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/guardrails.py +0 -0
  61. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/interrupt.py +0 -0
  62. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/llm_agent.py +0 -0
  63. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/logging.py +0 -0
  64. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/mcp/__init__.py +0 -0
  65. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/mcp/client.py +0 -0
  66. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/mcp/oauth.py +0 -0
  67. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/mcp/server.py +0 -0
  68. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/metrics.py +0 -0
  69. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/native_tool_use.py +0 -0
  70. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/operations.py +0 -0
  71. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/patches.py +0 -0
  72. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/pricing.py +0 -0
  73. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/produce.py +0 -0
  74. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/prompts.py +0 -0
  75. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/__init__.py +0 -0
  76. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/_retry.py +0 -0
  77. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/anthropic.py +0 -0
  78. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/azure.py +0 -0
  79. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/cerebras.py +0 -0
  80. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/chat.py +0 -0
  81. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/contracts.py +0 -0
  82. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/deepseek.py +0 -0
  83. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/fake.py +0 -0
  84. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/fireworks.py +0 -0
  85. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/gemini.py +0 -0
  86. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/github_models.py +0 -0
  87. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/groq.py +0 -0
  88. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/image.py +0 -0
  89. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/mistral.py +0 -0
  90. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/nvidia.py +0 -0
  91. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/ollama.py +0 -0
  92. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/openai.py +0 -0
  93. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/openrouter.py +0 -0
  94. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/perplexity.py +0 -0
  95. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/qwen.py +0 -0
  96. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/speech.py +0 -0
  97. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/together.py +0 -0
  98. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/video.py +0 -0
  99. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/xai.py +0 -0
  100. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/providers/zai.py +0 -0
  101. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/py.typed +0 -0
  102. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/__init__.py +0 -0
  103. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/_shared.py +0 -0
  104. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/agent.py +0 -0
  105. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/chat.py +0 -0
  106. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/models.py +0 -0
  107. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/rag.py +0 -0
  108. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quick/tools_agent.py +0 -0
  109. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/quota.py +0 -0
  110. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/__init__.py +0 -0
  111. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/cleanup.py +0 -0
  112. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/conversation.py +0 -0
  113. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/identity.py +0 -0
  114. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/inputs.py +0 -0
  115. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/memory.py +0 -0
  116. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/plan_execute.py +0 -0
  117. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/reflection.py +0 -0
  118. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/resolve.py +0 -0
  119. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/rollback.py +0 -0
  120. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/search.py +0 -0
  121. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/skills.py +0 -0
  122. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/status.py +0 -0
  123. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/supervisor.py +0 -0
  124. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/text.py +0 -0
  125. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/recipes/tool_loop.py +0 -0
  126. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/redaction.py +0 -0
  127. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/relations.py +0 -0
  128. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/replay.py +0 -0
  129. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/request.py +0 -0
  130. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/resources.py +0 -0
  131. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/routing.py +0 -0
  132. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/runtime.py +0 -0
  133. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/scheduler.py +0 -0
  134. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/session.py +0 -0
  135. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact/SKILL.md +0 -0
  136. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  137. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  138. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  139. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  140. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  141. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  142. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  143. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  144. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  145. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  146. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  147. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  148. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  149. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  150. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  151. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/sources.py +0 -0
  152. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/streaming.py +0 -0
  153. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/structured.py +0 -0
  154. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/__init__.py +0 -0
  155. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/assertions.py +0 -0
  156. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/exceptions.py +0 -0
  157. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/fault.py +0 -0
  158. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/golden.py +0 -0
  159. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/lab.py +0 -0
  160. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/mock.py +0 -0
  161. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/pytest_plugin.py +0 -0
  162. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/record.py +0 -0
  163. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/testing/registry.py +0 -0
  164. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tool_use.py +0 -0
  165. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tools.py +0 -0
  166. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/_otlp.py +0 -0
  167. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/columns.py +0 -0
  168. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/langfuse.py +0 -0
  169. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/otlp.py +0 -0
  170. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/templates/sessions.html +0 -0
  171. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/templates/ui.html +0 -0
  172. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/tracing/web.py +0 -0
  173. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/triggers.py +0 -0
  174. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/types.py +0 -0
  175. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/verify.py +0 -0
  176. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/viz.py +0 -0
  177. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact/web.py +0 -0
  178. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact.egg-info/SOURCES.txt +0 -0
  179. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact.egg-info/dependency_links.txt +0 -0
  180. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact.egg-info/entry_points.txt +0 -0
  181. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact.egg-info/requires.txt +0 -0
  182. {reactifact-0.16.0 → reactifact-0.16.1}/reactifact.egg-info/top_level.txt +0 -0
  183. {reactifact-0.16.0 → reactifact-0.16.1}/setup.cfg +0 -0
  184. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_a2a.py +0 -0
  185. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_a2a_example.py +0 -0
  186. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_a2a_interop.py +0 -0
  187. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_absent_consume.py +0 -0
  188. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_adaptive.py +0 -0
  189. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_agent_tool.py +0 -0
  190. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_anthropic_provider.py +0 -0
  191. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_artifacts.py +0 -0
  192. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_audit.py +0 -0
  193. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_authz.py +0 -0
  194. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_backbone.py +0 -0
  195. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_branching.py +0 -0
  196. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_budget.py +0 -0
  197. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_cache.py +0 -0
  198. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_canonical_ports.py +0 -0
  199. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_chat_example.py +0 -0
  200. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_chat_memory.py +0 -0
  201. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_chat_outbox.py +0 -0
  202. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_chat_web.py +0 -0
  203. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_checkpoint.py +0 -0
  204. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_checkpoints_concurrency.py +0 -0
  205. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_cli.py +0 -0
  206. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_cli_scenario.py +0 -0
  207. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_commit_log.py +0 -0
  208. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_compaction.py +0 -0
  209. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_concurrency.py +0 -0
  210. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_consumes_produces.py +0 -0
  211. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_context_builder.py +0 -0
  212. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_correlated_consume.py +0 -0
  213. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_debounce.py +0 -0
  214. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_determinism.py +0 -0
  215. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_devops.py +0 -0
  216. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_devops_online.py +0 -0
  217. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_devops_web.py +0 -0
  218. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_effects.py +0 -0
  219. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_eval.py +0 -0
  220. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_eval_dataset.py +0 -0
  221. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_eval_judge.py +0 -0
  222. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_eval_online.py +0 -0
  223. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_eval_trajectory.py +0 -0
  224. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_fintech_audit.py +0 -0
  225. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_forklab.py +0 -0
  226. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_forklab_web.py +0 -0
  227. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_friendly_api.py +0 -0
  228. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_gemini_provider.py +0 -0
  229. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_golden.py +0 -0
  230. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_guardrails.py +0 -0
  231. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_hitl.py +0 -0
  232. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_image_provider.py +0 -0
  233. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_incident_commander.py +0 -0
  234. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_invalidation.py +0 -0
  235. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_join_consume.py +0 -0
  236. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_knowledge.py +0 -0
  237. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_knowledge_web.py +0 -0
  238. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_llm_ladder.py +0 -0
  239. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_logging.py +0 -0
  240. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_loop_bound_client.py +0 -0
  241. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_mcp.py +0 -0
  242. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_medic_lab.py +0 -0
  243. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_medic_lab_web.py +0 -0
  244. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_metrics.py +0 -0
  245. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_multisource.py +0 -0
  246. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_native_tool_use.py +0 -0
  247. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_openai_provider.py +0 -0
  248. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_outbox.py +0 -0
  249. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_patches.py +0 -0
  250. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_pending_actions.py +0 -0
  251. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_ports.py +0 -0
  252. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_produce_styles.py +0 -0
  253. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_produce_trigger.py +0 -0
  254. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_prompts.py +0 -0
  255. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_provider_auth.py +0 -0
  256. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_provider_retry.py +0 -0
  257. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_providers_integration.py +0 -0
  258. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_quick.py +0 -0
  259. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_quota.py +0 -0
  260. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_reacts_to.py +0 -0
  261. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipe_conversation.py +0 -0
  262. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipe_plan_execute.py +0 -0
  263. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipe_reflection.py +0 -0
  264. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipe_supervisor.py +0 -0
  265. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipe_tool_loop.py +0 -0
  266. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipes.py +0 -0
  267. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipes_inputs.py +0 -0
  268. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipes_memory.py +0 -0
  269. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_recipes_skills.py +0 -0
  270. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_redaction.py +0 -0
  271. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_relation_graph.py +0 -0
  272. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_relations.py +0 -0
  273. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_repair.py +0 -0
  274. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_repair_web.py +0 -0
  275. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_replay.py +0 -0
  276. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_repo_agent.py +0 -0
  277. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_request.py +0 -0
  278. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_research.py +0 -0
  279. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_resources.py +0 -0
  280. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_retry.py +0 -0
  281. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_routing.py +0 -0
  282. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_runtime.py +0 -0
  283. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_runtime_errors.py +0 -0
  284. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_scheduler_semantics.py +0 -0
  285. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_session_append.py +0 -0
  286. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_sessions.py +0 -0
  287. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_shutdown.py +0 -0
  288. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_skills.py +0 -0
  289. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_sources.py +0 -0
  290. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_sources_search.py +0 -0
  291. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_sources_vector.py +0 -0
  292. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_speech_provider.py +0 -0
  293. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_starter_app.py +0 -0
  294. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_streaming.py +0 -0
  295. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_structured.py +0 -0
  296. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_support_copilot.py +0 -0
  297. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_assertions.py +0 -0
  298. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_golden.py +0 -0
  299. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_lab.py +0 -0
  300. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_mock.py +0 -0
  301. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_provenance.py +0 -0
  302. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_pytest_plugin.py +0 -0
  303. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_testing_registry.py +0 -0
  304. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_token_budget.py +0 -0
  305. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_tools.py +0 -0
  306. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_trace_columns.py +0 -0
  307. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_trace_tags.py +0 -0
  308. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_type_registry.py +0 -0
  309. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_vendor_factories.py +0 -0
  310. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_vendor_multimodal_factories.py +0 -0
  311. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_verify.py +0 -0
  312. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_video_provider.py +0 -0
  313. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_view.py +0 -0
  314. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_viz.py +0 -0
  315. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_web_source.py +0 -0
  316. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_workspace.py +0 -0
  317. {reactifact-0.16.0 → reactifact-0.16.1}/tests/test_workspace_with_sources.py +0 -0
  318. {reactifact-0.16.0 → reactifact-0.16.1}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.16.0
3
+ Version: 0.16.1
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.16.0"
7
+ version = "0.16.1"
8
8
  description = "Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -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.16.1"
55
55
 
56
56
  __all__ = [
57
57
  "Agent",
@@ -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,
@@ -30,6 +31,7 @@ from .store import (
30
31
  from .tracer import CompositeTracer, RecordingLLM, RunTracer, Tracer
31
32
 
32
33
  __all__ = [
34
+ "AgentContract",
33
35
  "AgentSpan",
34
36
  "ArtifactRef",
35
37
  "CompositeTracer",
@@ -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
 
@@ -113,6 +127,10 @@ class AgentSpan(BaseModel):
113
127
  #: ``Consume``) — e.g. ``DraftAnswer`` vs ``FinalResponse`` for an agent that
114
128
  #: consumes both. ``event_type`` alone (created/updated) can't tell them apart.
115
129
  event_artifact_type: str = ""
130
+ #: The concrete artifact whose change triggered this span.
131
+ event_artifact_id: str = ""
132
+ #: The agent's declared consumes/produces/tools (for the UI contract view).
133
+ contract: AgentContract = Field(default_factory=AgentContract)
116
134
  #: The produces that ran for this event, in execution order.
117
135
  produces: list[ProduceRun] = Field(default_factory=list)
118
136
  #: Tool executions that happened during this agent run (name/latency/status).
@@ -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,
@@ -81,7 +82,9 @@ class PostgresStore:
81
82
  relations JSONB NOT NULL DEFAULT '[]',
82
83
  llm_calls JSONB NOT NULL DEFAULT '[]',
83
84
  produces JSONB NOT NULL DEFAULT '[]',
84
- tools JSONB NOT NULL DEFAULT '[]'
85
+ tools JSONB NOT NULL DEFAULT '[]',
86
+ event_artifact_id TEXT NOT NULL DEFAULT '',
87
+ contract JSONB NOT NULL DEFAULT '{}'
85
88
  )
86
89
  """
87
90
  )
@@ -139,6 +142,14 @@ class PostgresStore:
139
142
  "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
140
143
  "tools JSONB NOT NULL DEFAULT '[]'"
141
144
  )
145
+ await cur.execute(
146
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
147
+ "event_artifact_id TEXT NOT NULL DEFAULT ''"
148
+ )
149
+ await cur.execute(
150
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
151
+ "contract JSONB NOT NULL DEFAULT '{}'"
152
+ )
142
153
  await conn.commit()
143
154
  finally:
144
155
  await conn.close()
@@ -178,8 +189,8 @@ class PostgresStore:
178
189
  await cur.execute(
179
190
  "INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
180
191
  "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)",
192
+ "produces, tools, event_artifact_id, contract) "
193
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
183
194
  (
184
195
  trace.id,
185
196
  span.agent,
@@ -206,6 +217,10 @@ class PostgresStore:
206
217
  psycopg.types.json.Jsonb(
207
218
  [tr.model_dump(mode="json") for tr in span.tools]
208
219
  ),
220
+ span.event_artifact_id,
221
+ psycopg.types.json.Jsonb(
222
+ span.contract.model_dump(mode="json")
223
+ ),
209
224
  ),
210
225
  )
211
226
  await conn.commit()
@@ -413,7 +428,8 @@ class PostgresStore:
413
428
  return None
414
429
  await cur.execute(
415
430
  "SELECT agent, event_type, event_artifact_type, latency_ms, error, "
416
- "started_at, reads, writes, relations, llm_calls, produces, tools "
431
+ "started_at, reads, writes, relations, llm_calls, produces, tools, "
432
+ "event_artifact_id, contract "
417
433
  "FROM spans WHERE run_id = %s ORDER BY id",
418
434
  (trace_id,),
419
435
  )
@@ -443,6 +459,8 @@ class PostgresStore:
443
459
  llm_calls=[LLMCall(**d) for d in r[9]],
444
460
  produces=[ProduceRun(**d) for d in r[10]],
445
461
  tools=[ToolRun(**d) for d in r[11]],
462
+ event_artifact_id=r[12],
463
+ contract=AgentContract(**r[13]),
446
464
  )
447
465
  for r in span_rows
448
466
  ]
@@ -28,6 +28,7 @@ from typing import Any, Protocol
28
28
  from ..tools import ToolRun
29
29
  from .columns import SpanArtifacts, TraceColumn, extract_columns
30
30
  from .models import (
31
+ AgentContract,
31
32
  AgentSpan,
32
33
  ArtifactRef,
33
34
  LLMCall,
@@ -185,7 +186,9 @@ class TraceStore:
185
186
  relations TEXT NOT NULL DEFAULT '[]',
186
187
  llm_calls TEXT NOT NULL DEFAULT '[]',
187
188
  produces TEXT NOT NULL DEFAULT '[]',
188
- tools TEXT NOT NULL DEFAULT '[]'
189
+ tools TEXT NOT NULL DEFAULT '[]',
190
+ event_artifact_id TEXT NOT NULL DEFAULT '',
191
+ contract TEXT NOT NULL DEFAULT '{}'
189
192
  );
190
193
  CREATE TABLE IF NOT EXISTS tags (
191
194
  name TEXT PRIMARY KEY,
@@ -235,6 +238,14 @@ class TraceStore:
235
238
  self._conn.execute(
236
239
  "ALTER TABLE spans ADD COLUMN tools TEXT NOT NULL DEFAULT '[]'"
237
240
  )
241
+ if "event_artifact_id" not in cols:
242
+ self._conn.execute(
243
+ "ALTER TABLE spans ADD COLUMN event_artifact_id TEXT NOT NULL DEFAULT ''"
244
+ )
245
+ if "contract" not in cols:
246
+ self._conn.execute(
247
+ "ALTER TABLE spans ADD COLUMN contract TEXT NOT NULL DEFAULT '{}'"
248
+ )
238
249
  run_cols = {row[1] for row in self._conn.execute("PRAGMA table_info(runs)")}
239
250
  if "prompt_tokens" not in run_cols:
240
251
  self._conn.execute(
@@ -347,8 +358,8 @@ class TraceStore:
347
358
  self._conn.execute(
348
359
  "INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
349
360
  "latency_ms, error, started_at, reads, writes, relations, llm_calls, "
350
- "produces, tools) "
351
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
361
+ "produces, tools, event_artifact_id, contract) "
362
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
352
363
  (
353
364
  trace.id,
354
365
  span.agent,
@@ -363,6 +374,8 @@ class TraceStore:
363
374
  json.dumps([c.model_dump(mode="json") for c in span.llm_calls]),
364
375
  json.dumps([p.model_dump(mode="json") for p in span.produces]),
365
376
  json.dumps([t.model_dump(mode="json") for t in span.tools]),
377
+ span.event_artifact_id,
378
+ json.dumps(span.contract.model_dump(mode="json")),
366
379
  ),
367
380
  )
368
381
  self._conn.commit()
@@ -617,7 +630,8 @@ class TraceStore:
617
630
  return None
618
631
  span_rows = self._conn.execute(
619
632
  "SELECT agent, event_type, event_artifact_type, latency_ms, error, started_at, "
620
- "reads, writes, relations, llm_calls, produces, tools "
633
+ "reads, writes, relations, llm_calls, produces, tools, "
634
+ "event_artifact_id, contract "
621
635
  "FROM spans WHERE run_id = ? ORDER BY id",
622
636
  (trace_id,),
623
637
  ).fetchall()
@@ -637,6 +651,8 @@ class TraceStore:
637
651
  llm_calls=[LLMCall(**d) for d in json.loads(r[9])],
638
652
  produces=[ProduceRun(**d) for d in json.loads(r[10])],
639
653
  tools=[ToolRun(**d) for d in json.loads(r[11])],
654
+ event_artifact_id=r[12],
655
+ contract=AgentContract(**json.loads(r[13])),
640
656
  )
641
657
  for r in span_rows
642
658
  ]
@@ -6,6 +6,7 @@
6
6
  --text:#17171f; --muted:#6b6b7a; --faint:#9a9aae;
7
7
  --accent:#4f46e5; --accent-soft:#eef0ff; --accent-strong:#4338ca;
8
8
  --ok:#15803d; --ok-soft:#e9f7ef; --err:#b91c1c; --err-soft:#fdeeee;
9
+ --tool:#b45309; --tool-soft:#fdf1e3;
9
10
  --warn:#b45309; --warn-soft:#fef3c7; --hover:#f3f3f7; --chip:#eef0ff; --chip-text:#4338ca;
10
11
  --shadow-sm:0 1px 2px rgba(16,16,30,.05);
11
12
  --shadow:0 1px 2px rgba(16,16,30,.04),0 4px 14px rgba(16,16,30,.06);
@@ -20,6 +21,7 @@ html.dark{
20
21
  --text:#e8e8f0; --muted:#9a9aae; --faint:#6f6f82;
21
22
  --accent:#8b93ff; --accent-soft:#232448; --accent-strong:#a9b1ff;
22
23
  --ok:#3ecf7c; --ok-soft:#123522; --err:#f97c7c; --err-soft:#3c1718;
24
+ --tool:#fbbf24; --tool-soft:#3a2a10;
23
25
  --warn:#fbbf24; --warn-soft:#3a2c0c; --hover:#1e1f29; --chip:#262a48; --chip-text:#a9b1ff;
24
26
  --shadow-sm:0 1px 2px rgba(0,0,0,.4);
25
27
  --shadow:0 1px 2px rgba(0,0,0,.4),0 8px 24px rgba(0,0,0,.35);
@@ -274,6 +276,24 @@ details.agent>summary:hover{background:var(--hover);}
274
276
  .chip.produce.noop{background:var(--panel-alt);color:var(--muted);border:1px solid var(--border);}
275
277
  .chip.produce .ops{background:rgba(21,128,61,.16);color:var(--ok);border-radius:7px;padding:0 6px;font-size:10px;font-variant-numeric:tabular-nums;}
276
278
  .chip.produce.noop .ops.zero{background:var(--border);color:var(--faint);}
279
+ .chip.tool{background:var(--tool-soft);color:var(--tool);}
280
+ .consume{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);font-size:11.5px;color:var(--chip-text);background:var(--chip);border-radius:6px;padding:1px 7px;white-space:nowrap;}
281
+ .consume.muted{color:var(--muted);background:transparent;border:1px dashed var(--border-strong);}
282
+ .tl-track-wrap{min-width:0;}
283
+ .chip.muted,.consume.muted{opacity:.55;}
284
+ [data-filter-kind]{cursor:pointer;}
285
+ [data-filter-kind].active{outline:2px solid var(--accent);outline-offset:1px;}
286
+ .contract-flow{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0;}
287
+ .cf-label{font-size:10px;text-transform:uppercase;letter-spacing:.45px;color:var(--muted);white-space:nowrap;}
288
+ .trigger{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--muted);white-space:nowrap;}
289
+ .trigger code{font-size:10.5px;}
290
+ .produce-row{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px;}
291
+ .produce-row.muted{opacity:.6;}
292
+ .filter-note{display:flex;align-items:center;gap:8px;margin:0 0 10px;font-size:12px;color:var(--muted);}
293
+ .tl-subs{position:relative;height:8px;margin-top:2px;}
294
+ .tl-sub{position:absolute;top:0;height:6px;border-radius:3px;opacity:.9;min-width:2px;}
295
+ .tl-sub.produce{background:var(--ok);}
296
+ .tl-sub.tool{background:var(--tool);}
277
297
  .spans{display:flex;flex-direction:column;gap:8px;}
278
298
  details.node{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--r-sm);background:var(--panel);overflow:hidden;}
279
299
  details.node summary{display:flex;align-items:center;gap:10px;padding:9px 14px;cursor:pointer;font-size:13px;}
@@ -57,6 +57,7 @@
57
57
  <h2>Work</h2>
58
58
  <div class="spacer"></div>
59
59
  <input class="input" id="trace-search" type="search" placeholder="search spans…" style="min-width:180px"/>
60
+ <span id="chip-filter" class="hint"></span>
60
61
  <div class="seg" id="view-seg">
61
62
  <button class="seg-btn active" data-view="tree">Tree</button>
62
63
  <button class="seg-btn" data-view="timeline">Timeline</button>
@@ -186,7 +187,8 @@ function artsBlock(arts){
186
187
  return chips+'<div class="art-list">'+first+'<details class="io-more" open><summary>… + '+(n-SHOWN)+' more</summary>'+rest+'</details></div>';
187
188
  }
188
189
 
189
- let TRACE=null,VIEW='tree',FILTER='';
190
+ let TRACE=null,VIEW='tree',FILTER='',CHIP_FILTER=null;
191
+ const AGENT_CONTRACTS={};
190
192
  function render(t){
191
193
  TRACE=t;
192
194
  document.title='Trace #'+t.id+' · reactifact';
@@ -218,8 +220,30 @@ function renderFields(fields){
218
220
  '<div class="fv'+(empty?' empty':'')+'">'+escapeHtml(empty?'—':String(v))+'</div></div>';
219
221
  }).join('');
220
222
  }
223
+ function uniq(a){return [...new Set(a.filter(Boolean))];}
224
+ function shortId(id){return id?String(id).slice(0,8):'';}
225
+ function chipHtml(kind,value,opts){
226
+ opts=opts||{};
227
+ const cls=kind==='consume'?'consume':'chip '+kind;
228
+ const active=CHIP_FILTER&&CHIP_FILTER.kind===kind&&CHIP_FILTER.value===value;
229
+ const count=opts.count?' <span class="ops">'+opts.count+'</span>':'';
230
+ return '<span class="'+cls+(opts.muted?' muted':'')+(active?' active':'')+'" '+
231
+ 'data-filter-kind="'+kind+'" data-filter-value="'+escapeHtml(value)+'" '+
232
+ 'title="'+escapeHtml(opts.title||value)+'">'+escapeHtml(value)+count+'</span>';
233
+ }
234
+ function contractOf(agent,list){
235
+ const withContract=list.find(s=>s.contract&&((s.contract.consumes||[]).length||(s.contract.produces||[]).length||(s.contract.tools||[]).length));
236
+ return withContract?withContract.contract:{consumes:[],produces:[],tools:[],priority:0};
237
+ }
221
238
  function spanMatches(s,f){
222
- const hay=[s.agent,s.event_type,s.event_artifact_type||'',s.error||''];
239
+ if(CHIP_FILTER){
240
+ const k=CHIP_FILTER.kind,v=CHIP_FILTER.value,c=s.contract||{};
241
+ if(k==='consume'&&!(s.event_artifact_type===v||(s.reads||[]).some(a=>a.data_type===v)||(c.consumes||[]).includes(v)))return false;
242
+ if(k==='produce'&&!((s.produces||[]).some(p=>p.name===v)||(c.produces||[]).includes(v)))return false;
243
+ if(k==='tool'&&!((s.tools||[]).some(t=>t.tool===v)||(c.tools||[]).includes(v)))return false;
244
+ }
245
+ if(!f)return true;
246
+ const hay=[s.agent,s.event_type,s.event_artifact_type||'',s.event_artifact_id||'',s.error||''];
223
247
  for(const p of (s.produces||[]))hay.push(p.name||'');
224
248
  for(const a of (s.reads||[]).concat(s.writes||[]))hay.push(a.data_type||'',a.data||'');
225
249
  for(const c of (s.llm_calls||[]))hay.push(c.provider||'',c.model||'',c.response||'',...(c.messages||[]).map(m=>m.content||''));
@@ -227,7 +251,11 @@ function spanMatches(s,f){
227
251
  }
228
252
  function renderWork(){
229
253
  const spans=(TRACE&&TRACE.spans)||[];const f=FILTER.trim().toLowerCase();
230
- const filtered=f?spans.filter(s=>spanMatches(s,f)):spans;
254
+ const filtered=spans.filter(s=>spanMatches(s,f));
255
+ const cf=document.getElementById('chip-filter');
256
+ if(cf)cf.innerHTML=CHIP_FILTER
257
+ ? 'filter: '+escapeHtml(CHIP_FILTER.kind)+' '+escapeHtml(CHIP_FILTER.value)+' <button class="btn sm" data-clear-filter>clear</button>'
258
+ : '';
231
259
  if(VIEW==='timeline')renderTimeline(filtered);else renderTree(filtered);
232
260
  }
233
261
  function renderTree(spans){
@@ -253,57 +281,85 @@ function renderTimeline(spans){
253
281
  const prods=(s.produces||[]).map(p=>p.name).join('+');
254
282
  const toolCount=(s.tools||[]).length;
255
283
  const label=escapeHtml(s.agent)+' · '+escapeHtml(s.event_artifact_type||s.event_type||'span')+(prods?' → '+escapeHtml(prods):'')+(toolCount?' · '+toolCount+' tools':'');const llm=(s.llm_calls||[]).length;
284
+ let subs='';let pcursor=r.start;
285
+ for(const p of (s.produces||[])){
286
+ const w=Math.max(0.3,(Number(p.latency_ms)||0)/total*100);
287
+ subs+='<span class="tl-sub produce" style="left:'+(pcursor/total*100)+'%;width:'+w+'%" title="'+escapeHtml(p.name)+' — '+Number(p.latency_ms||0).toFixed(1)+' ms"></span>';
288
+ pcursor+=Number(p.latency_ms)||0;
289
+ }
290
+ for(const tool of (s.tools||[])){
291
+ let ts=r.start;
292
+ if(tool.started_at){const d=new Date(tool.started_at).getTime()-origin;if(isFinite(d))ts=Math.max(0,d);}
293
+ const w=Math.max(0.3,(Number(tool.latency_ms)||0)/total*100);
294
+ subs+='<span class="tl-sub tool" style="left:'+(ts/total*100)+'%;width:'+w+'%" title="'+escapeHtml(tool.tool)+' — '+Number(tool.latency_ms||0).toFixed(1)+' ms"></span>';
295
+ }
256
296
  html+='<div class="tl-row"><div class="tl-label" title="'+label+'">'+label+(llm?' <span class="sub">'+llm+' llm</span>':'')+'</div>'+
257
- '<div class="tl-track"><div class="tl-bar'+(s.error?' err':'')+'" style="left:'+left+'%;width:'+width+'%" title="'+label+' — '+r.dur.toFixed(1)+' ms"><span class="lbl">'+r.dur.toFixed(0)+' ms</span></div></div></div>';
297
+ '<div class="tl-track-wrap"><div class="tl-track"><div class="tl-bar'+(s.error?' err':'')+'" style="left:'+left+'%;width:'+width+'%" title="'+label+' — '+r.dur.toFixed(1)+' ms"><span class="lbl">'+r.dur.toFixed(0)+' ms</span></div></div>'+
298
+ (subs?'<div class="tl-subs">'+subs+'</div>':'')+'</div></div>';
258
299
  }
259
300
  el.innerHTML=html+'</div>';
260
301
  }
261
302
  function agentHtml(agent,list){
262
- const ms=list.reduce((a,s)=>a+Number(s.latency_ms||0),0)/list.length;let writes=0,llm=0;const readsAgg={};
263
- for(const s of list){writes+=(s.writes||[]).length;llm+=(s.llm_calls||[]).length;
264
- for(const r of s.reads||[]){const t=r.data_type||'artifact';readsAgg[t]=(readsAgg[t]||0)+1;}}
265
- const chips=Object.entries(readsAgg).map(([t,n])=>'<span class="chip">'+escapeHtml(t)+' '+n+'</span>').join('');
266
- const consumes=[...new Set(list.map(s=>s.event_artifact_type).filter(Boolean))];
267
- const prods=[...new Set(list.flatMap(s=>(s.produces||[]).map(p=>p.name)))];
268
- const flow=(consumes.length||prods.length)
269
- ? '<span class="flow"><span class="c-side">'+consumes.map(c=>'<span class="consume">'+escapeHtml(c)+'</span>').join('')+
270
- '</span><span class="arrow">→</span><span class="produces">'+prods.map(n=>'<span class="chip produce">'+escapeHtml(n)+'</span>').join('')+'</span></span>'
271
- : '';
303
+ const contract=contractOf(agent,list);AGENT_CONTRACTS[agent]=contract;
304
+ const actualConsumes=uniq(list.flatMap(s=>[s.event_artifact_type,...(s.reads||[]).map(r=>r.data_type)]));
305
+ const actualProduces=uniq(list.flatMap(s=>(s.produces||[]).map(p=>p.name)));
306
+ const actualTools=uniq(list.flatMap(s=>(s.tools||[]).map(t=>t.tool)));
307
+ const consumes=uniq([...(contract.consumes||[]),...actualConsumes]);
308
+ const produces=uniq([...(contract.produces||[]),...actualProduces]);
309
+ const tools=uniq([...(contract.tools||[]),...actualTools]);
310
+ const consumeChips=consumes.map(v=>chipHtml('consume',v,{muted:!actualConsumes.includes(v)})).join('');
311
+ const produceChips=produces.map(v=>chipHtml('produce',v,{muted:!actualProduces.includes(v)})).join('');
312
+ const toolChips=tools.map(v=>chipHtml('tool',v,{muted:!actualTools.includes(v)})).join('');
313
+ let writes=0,llm=0,total=0;
314
+ for(const s of list){writes+=(s.writes||[]).length;llm+=(s.llm_calls||[]).length;total+=Number(s.latency_ms||0);}
315
+ const flow='<span class="contract-flow"><span class="cf-label">consumes</span>'+consumeChips+
316
+ '<span class="arrow">→</span><span class="cf-label">produces</span>'+produceChips+'</span>';
272
317
  return '<details class="agent" open><summary><span class="caret">▸</span><b>'+escapeHtml(agent)+'</b>'+
273
318
  flow+
274
- '<span class="muted">'+list.length+' spans · '+ms.toFixed(1)+' ms avg</span>'+
275
- '<span class="stat">'+writes+' writes · '+llm+' llm</span></summary>'+
276
- '<div class="agent-body">'+(chips?'<div class="chips">'+chips+'</div>':'')+
277
- '<div class="spans">'+list.map(spanHtml).join('')+'</div></div></details>';
319
+ '<span class="muted">'+list.length+' spans · '+(total/list.length).toFixed(1)+' ms avg</span>'+
320
+ '<span class="stat">'+writes+' writes · '+llm+' llm'+(actualTools.length?' · '+actualTools.length+' tools':'')+'</span>'+
321
+ '<button class="btn sm" data-contract-agent="'+escapeHtml(agent)+'">contract</button></summary>'+
322
+ '<div class="agent-body">'+
323
+ (toolChips?'<div class="chips"><span class="cf-label">tools</span>'+toolChips+'</div>':'')+
324
+ '<div class="spans">'+list.map(spanHtml).join('')+'</div></div></details>';
278
325
  }
279
326
  function spanHtml(s){
280
327
  const ok=!s.error;const errBlock=s.error?'<div class="err-box">'+escapeHtml(s.error)+'</div>':'';
281
- const reads=(s.reads||[]).length?('<div class="io"><h3>reads</h3>'+artsBlock(s.reads)+'</div>'):'';
282
- const writes=(s.writes||[]).length?('<div class="io"><h3>writes</h3>'+artsBlock(s.writes)+'</div>'):'';
328
+ const c=s.contract||{};
329
+ const actualConsumes=uniq([s.event_artifact_type,...(s.reads||[]).map(r=>r.data_type)]);
330
+ const actualProduces=uniq((s.produces||[]).map(p=>p.name));
331
+ const consumes=uniq([...(c.consumes||[]),...actualConsumes]);
332
+ const produces=uniq([...(c.produces||[]),...actualProduces]);
333
+ const consumeChips=consumes.map(v=>chipHtml('consume',v,{muted:!actualConsumes.includes(v)})).join('');
334
+ const produceChips=produces.map(v=>chipHtml('produce',v,{muted:!actualProduces.includes(v)})).join('');
335
+ const trigger=s.event_artifact_type
336
+ ? '<span class="trigger">triggered by '+chipHtml('consume',s.event_artifact_type,{title:'artifact '+(s.event_artifact_id||'')})+
337
+ (s.event_artifact_id?' <code>'+escapeHtml(shortId(s.event_artifact_id))+'</code>':'')+'</span>'
338
+ : '<span class="trigger">event '+escapeHtml(s.event_type||'')+'</span>';
339
+ const flow='<span class="contract-flow"><span class="cf-label">consumes</span>'+consumeChips+
340
+ '<span class="arrow">→</span><span class="cf-label">produces</span>'+produceChips+'</span>';
341
+ const reads=(s.reads||[]).length?('<div class="io"><h3>Consumes / inputs</h3>'+artsBlock(s.reads)+'</div>'):'';
342
+ const writes=(s.writes||[]).length?('<div class="io"><h3>Outputs / writes</h3>'+artsBlock(s.writes)+'</div>'):'';
283
343
  const io=(reads||writes)?'<div class="io-grid">'+reads+writes+'</div>':'';
284
344
  const llm=(s.llm_calls||[]).map(llmHtml).join('');
285
345
  const toolCount=(s.tools||[]).length;
286
346
  return '<details class="node"><summary>'+
287
347
  '<span class="caret">▸</span><span class="s-dot '+(ok?'ok':'err')+'"></span>'+
288
- '<span class="nid">'+escapeHtml(s.agent)+'</span>'+
289
- flowHtml(s.event_artifact_type, s.event_type, s.produces)+
348
+ '<span class="nid">'+escapeHtml(s.agent)+'</span>'+trigger+
349
+ flow+
290
350
  '<span class="stat">'+Number(s.latency_ms).toFixed(1)+' ms · '+(s.llm_calls||[]).length+' llm'+(toolCount?' · '+toolCount+' tools':'')+(ok?'':' · failed')+'</span></summary>'+
291
- '<div class="node-body">'+errBlock+io+toolsBlock(s.tools)+(llm?'<div class="llm"><h3>LLM calls</h3>'+llm+'</div>':'')+'</div></details>';
351
+ '<div class="node-body">'+errBlock+io+producesBlock(s.produces,c)+toolsBlock(s.tools)+(llm?'<div class="llm"><h3>LLM calls</h3>'+llm+'</div>':'')+'</div></details>';
292
352
  }
293
- function flowHtml(consume, eventType, produces){
294
- const c=consume
295
- ? '<span class="consume" title="Consume — artifact whose change triggered this agent">'+escapeHtml(consume)+'</span>'
296
- : '<span class="consume muted">event</span>';
297
- const ev=eventType?'<span class="ev">'+escapeHtml(eventType)+'</span>':'';
298
- const list=produces||[];
299
- const right=list.length?list.map(produceChip).join(''):'<span class="muted">no produce ran</span>';
300
- return '<span class="flow"><span class="c-side">'+c+ev+'</span>'+
301
- '<span class="arrow" aria-hidden="true">→</span><span class="produces">'+right+'</span></span>';
302
- }
303
- function produceChip(p){
304
- const ops=Number(p.operations||0);const ms=Number(p.latency_ms||0);
305
- return '<span class="chip produce'+(ops?'':' noop')+'" title="'+escapeHtml(p.name)+' — '+ms.toFixed(1)+' ms">'+
306
- escapeHtml(p.name)+'<span class="ops'+(ops?'':' zero')+'">'+(ops?('+'+ops):'0')+'</span></span>';
353
+ function producesBlock(list,contract){
354
+ const actual=list||[];const declared=(contract&&contract.produces)||[];
355
+ const names=uniq([...declared,...actual.map(p=>p.name)]);
356
+ if(!names.length)return '';
357
+ const rows=names.map(n=>{
358
+ const p=actual.find(x=>x.name===n);
359
+ if(!p)return '<div class="produce-row muted"><span class="chip produce muted">'+escapeHtml(n)+'</span><span class="muted">did not run</span></div>';
360
+ return '<div class="produce-row"><span class="chip produce" data-filter-kind="produce" data-filter-value="'+escapeHtml(n)+'">'+escapeHtml(n)+'</span><span class="muted">'+Number(p.operations||0)+' ops · '+Number(p.latency_ms||0).toFixed(1)+' ms</span></div>';
361
+ }).join('');
362
+ return '<div class="llm"><h3>Produces</h3>'+rows+'</div>';
307
363
  }
308
364
  function artHtml(a){
309
365
  const op=('<span class="tag">'+escapeHtml(a.op_type||'read')+'</span>');
@@ -453,10 +509,26 @@ document.getElementById('data-modal-close').addEventListener('click',closeDataMo
453
509
  document.getElementById('data-modal').addEventListener('click',(e)=>{if(e.target.id==='data-modal')closeDataModal();});
454
510
  document.addEventListener('keydown',(e)=>{if(e.key==='Escape')closeDataModal();});
455
511
  document.getElementById('agents').addEventListener('click',(e)=>{
512
+ const chip=e.target.closest('[data-filter-kind]');
513
+ if(chip){
514
+ e.preventDefault();e.stopPropagation();
515
+ const kind=chip.getAttribute('data-filter-kind'),value=chip.getAttribute('data-filter-value');
516
+ CHIP_FILTER=(CHIP_FILTER&&CHIP_FILTER.kind===kind&&CHIP_FILTER.value===value)?null:{kind:kind,value:value};
517
+ renderWork();return;
518
+ }
519
+ const contractBtn=e.target.closest('[data-contract-agent]');
520
+ if(contractBtn){
521
+ e.preventDefault();e.stopPropagation();
522
+ const a=contractBtn.getAttribute('data-contract-agent');
523
+ openDataModal('Contract: '+a,JSON.stringify(AGENT_CONTRACTS[a]||{},null,2));return;
524
+ }
456
525
  const el=e.target.closest('[data-art-key]');if(!el)return;
457
526
  const key=el.getAttribute('data-art-key');
458
527
  if(ARTIFACTS[key]!==undefined)openDataModal('Artifact '+key,ARTIFACTS[key]);
459
528
  });
529
+ document.addEventListener('click',(e)=>{
530
+ if(e.target.closest('[data-clear-filter]')){CHIP_FILTER=null;renderWork();}
531
+ });
460
532
  document.getElementById('copy-link').addEventListener('click',()=>copyTo('copy-link',location.href,'link'));
461
533
  document.getElementById('copy-json').addEventListener('click',()=>copyTo('copy-json',TRACE?JSON.stringify(TRACE,null,2):'','json'));
462
534
  document.getElementById('toggle-json').addEventListener('click',(e)=>{
@@ -25,7 +25,15 @@ from typing import TYPE_CHECKING, Any
25
25
 
26
26
  from ..patches import Create, Link, Update
27
27
  from ..providers import LLMProvider, LLMRequest, LLMResponse, LLMResponseChunk
28
- from .models import AgentSpan, ArtifactRef, LLMCall, ProduceRun, RelationRef, RunTrace
28
+ from .models import (
29
+ AgentContract,
30
+ AgentSpan,
31
+ ArtifactRef,
32
+ LLMCall,
33
+ ProduceRun,
34
+ RelationRef,
35
+ RunTrace,
36
+ )
29
37
  from .store import TraceSink, TraceStore
30
38
 
31
39
  if TYPE_CHECKING:
@@ -460,6 +468,20 @@ class RunTracer:
460
468
  if self.tracer is None:
461
469
  return None
462
470
  try:
471
+ consumes = [
472
+ getattr(c.artifact_type, "__name__", str(c.artifact_type))
473
+ for c in (agent.consumes or [])
474
+ if c.artifact_type is not None
475
+ ]
476
+ produces_declared = [
477
+ getattr(p, "name", type(p).__name__) for p in (agent.produces or [])
478
+ ]
479
+ declared_tools = getattr(agent, "tools", ()) or ()
480
+ if isinstance(declared_tools, dict):
481
+ declared_tools = declared_tools.values()
482
+ tools_declared = [
483
+ getattr(t, "name", type(t).__name__) for t in declared_tools
484
+ ]
463
485
  span = AgentSpan(
464
486
  agent=agent.name,
465
487
  event_type=event.type.value,
@@ -468,6 +490,13 @@ class RunTracer:
468
490
  if isinstance(event.artifact_type, type)
469
491
  else str(event.artifact_type)
470
492
  ),
493
+ event_artifact_id=event.artifact_id,
494
+ contract=AgentContract(
495
+ consumes=consumes,
496
+ produces=produces_declared,
497
+ tools=tools_declared,
498
+ priority=agent.priority,
499
+ ),
471
500
  produces=[
472
501
  ProduceRun(name=name, operations=operations, latency_ms=latency)
473
502
  for name, operations, latency in (produces or [])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.16.0
3
+ Version: 0.16.1
4
4
  Summary: Reactive, artifact-driven agent runtime: agents transform versioned, typed, provenance-aware artifacts inside an evolving context
5
5
  License-Expression: MIT
6
6
  Project-URL: Homepage, https://github.com/bzdvdn/reactifact
@@ -249,6 +249,7 @@ FROZEN: dict[str, frozenset[str]] = {
249
249
  ),
250
250
  "reactifact.tracing": frozenset(
251
251
  {
252
+ "AgentContract",
252
253
  "AgentSpan",
253
254
  "ArtifactRef",
254
255
  "CompositeTracer",
@@ -17,7 +17,7 @@ def test_span_renderer_defines_the_tool_count_it_uses():
17
17
  threw a ReferenceError and left the whole Work section empty."""
18
18
  html = _ui_run_html()
19
19
  start = html.index("function spanHtml(s){")
20
- end = html.index("function flowHtml", start)
20
+ end = html.index("function producesBlock", start)
21
21
  body = html[start:end]
22
22
 
23
23
  assert "const toolCount" in body
@@ -31,3 +31,14 @@ def test_artifact_preview_opens_the_full_data_modal():
31
31
  assert "openDataModal" in html
32
32
  assert 'id="data-modal"' in html
33
33
  assert "const ARTIFACTS={}" in html
34
+
35
+
36
+ def test_work_ui_has_contract_flow_and_chip_filters():
37
+ html = _ui_run_html()
38
+ assert "contract-flow" in html
39
+ assert "data-contract-agent" in html
40
+ assert "data-filter-kind" in html
41
+ assert "Consumes / inputs" in html
42
+ assert "Outputs / writes" in html
43
+ assert "tl-sub produce" in html
44
+ assert "tl-sub tool" in html
@@ -146,6 +146,10 @@ def test_runtime_records_spans_and_trace(tmp_path):
146
146
  assert trace.outcome == "completed"
147
147
  # greeter span: read the question, created the answer
148
148
  greeter = next(s for s in trace.spans if s.agent == "Greeter")
149
+ assert greeter.event_artifact_id == ctx.list_artifacts(Question)[0].id
150
+ assert greeter.contract.consumes == ["Question"]
151
+ assert greeter.contract.produces == []
152
+ assert greeter.contract.tools == []
149
153
  assert any(
150
154
  r.artifact_id == ctx.list_artifacts(Question)[0].id for r in greeter.reads
151
155
  )
@@ -187,6 +191,7 @@ def test_runtime_records_which_produce_ran(tmp_path):
187
191
  assert span.event_artifact_type == "Question"
188
192
  assert [p.name for p in span.produces] == ["Emit"]
189
193
  assert span.produces[0].operations >= 1
194
+ assert span.contract.produces == ["Emit", "NeverRuns"]
190
195
 
191
196
 
192
197
  def test_runtime_records_a_produce_that_raised(tmp_path):
@@ -1100,6 +1105,8 @@ def test_runtime_records_tool_runs(tmp_path):
1100
1105
  assert span.tools[0].chars == len("found")
1101
1106
  assert span.tools[0].produce == "UsesTool"
1102
1107
  assert span.tools[0].output == "found"
1108
+ assert span.contract.consumes == ["Question"]
1109
+ assert span.contract.produces == ["UsesTool"]
1103
1110
 
1104
1111
 
1105
1112
  def test_tool_args_are_opt_in_and_redacted(tmp_path):
File without changes
File without changes