reactifact 0.15.1__tar.gz → 0.15.2__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 (315) hide show
  1. {reactifact-0.15.1/reactifact.egg-info → reactifact-0.15.2}/PKG-INFO +2 -2
  2. {reactifact-0.15.1 → reactifact-0.15.2}/README.md +1 -1
  3. {reactifact-0.15.1 → reactifact-0.15.2}/pyproject.toml +1 -1
  4. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/__init__.py +1 -1
  5. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/agents.py +42 -1
  6. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/produce.py +35 -0
  7. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/runtime.py +24 -9
  8. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/__init__.py +2 -0
  9. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/langfuse.py +73 -0
  10. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/models.py +20 -0
  11. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/otlp.py +11 -0
  12. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/postgres.py +31 -13
  13. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/store.py +29 -13
  14. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/templates/app.css +12 -1
  15. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/templates/ui_run.html +30 -4
  16. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/tracer.py +13 -1
  17. {reactifact-0.15.1 → reactifact-0.15.2/reactifact.egg-info}/PKG-INFO +2 -2
  18. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_produce_styles.py +17 -0
  19. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_public_api.py +1 -0
  20. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_tracing.py +117 -2
  21. {reactifact-0.15.1 → reactifact-0.15.2}/LICENSE +0 -0
  22. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/__main__.py +0 -0
  23. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/_extras.py +0 -0
  24. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/_httpx.py +0 -0
  25. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/a2a/__init__.py +0 -0
  26. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/a2a/client.py +0 -0
  27. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/a2a/models.py +0 -0
  28. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/a2a/remote.py +0 -0
  29. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/a2a/server.py +0 -0
  30. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/agent_tool.py +0 -0
  31. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/artifacts.py +0 -0
  32. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/audit.py +0 -0
  33. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/authz.py +0 -0
  34. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/branching.py +0 -0
  35. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/budget.py +0 -0
  36. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cache.py +0 -0
  37. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/chat.py +0 -0
  38. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/checkpoints.py +0 -0
  39. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/__init__.py +0 -0
  40. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/branch.py +0 -0
  41. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/common.py +0 -0
  42. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/context.py +0 -0
  43. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/eval.py +0 -0
  44. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/graph.py +0 -0
  45. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/replay.py +0 -0
  46. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/scenario.py +0 -0
  47. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/skills.py +0 -0
  48. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/cli/trace.py +0 -0
  49. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/commit.py +0 -0
  50. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/commit_log.py +0 -0
  51. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/consume.py +0 -0
  52. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/context.py +0 -0
  53. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/context_builder.py +0 -0
  54. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/effects.py +0 -0
  55. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/__init__.py +0 -0
  56. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/dataset.py +0 -0
  57. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/evaluators.py +0 -0
  58. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/judge.py +0 -0
  59. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/online.py +0 -0
  60. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/runner.py +0 -0
  61. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/eval/scoring.py +0 -0
  62. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/events.py +0 -0
  63. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/guardrails.py +0 -0
  64. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/interrupt.py +0 -0
  65. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/llm_agent.py +0 -0
  66. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/logging.py +0 -0
  67. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/mcp/__init__.py +0 -0
  68. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/mcp/client.py +0 -0
  69. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/mcp/oauth.py +0 -0
  70. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/mcp/server.py +0 -0
  71. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/metrics.py +0 -0
  72. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/native_tool_use.py +0 -0
  73. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/operations.py +0 -0
  74. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/patches.py +0 -0
  75. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/pricing.py +0 -0
  76. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/prompts.py +0 -0
  77. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/__init__.py +0 -0
  78. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/_retry.py +0 -0
  79. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/anthropic.py +0 -0
  80. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/azure.py +0 -0
  81. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/cerebras.py +0 -0
  82. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/chat.py +0 -0
  83. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/contracts.py +0 -0
  84. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/deepseek.py +0 -0
  85. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/fake.py +0 -0
  86. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/fireworks.py +0 -0
  87. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/gemini.py +0 -0
  88. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/github_models.py +0 -0
  89. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/groq.py +0 -0
  90. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/image.py +0 -0
  91. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/mistral.py +0 -0
  92. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/nvidia.py +0 -0
  93. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/ollama.py +0 -0
  94. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/openai.py +0 -0
  95. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/openrouter.py +0 -0
  96. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/perplexity.py +0 -0
  97. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/qwen.py +0 -0
  98. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/speech.py +0 -0
  99. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/together.py +0 -0
  100. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/video.py +0 -0
  101. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/xai.py +0 -0
  102. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/providers/zai.py +0 -0
  103. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/py.typed +0 -0
  104. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/__init__.py +0 -0
  105. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/_shared.py +0 -0
  106. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/agent.py +0 -0
  107. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/chat.py +0 -0
  108. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/models.py +0 -0
  109. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/rag.py +0 -0
  110. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quick/tools_agent.py +0 -0
  111. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/quota.py +0 -0
  112. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/__init__.py +0 -0
  113. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/cleanup.py +0 -0
  114. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/conversation.py +0 -0
  115. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/identity.py +0 -0
  116. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/inputs.py +0 -0
  117. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/memory.py +0 -0
  118. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/plan_execute.py +0 -0
  119. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/reflection.py +0 -0
  120. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/resolve.py +0 -0
  121. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/rollback.py +0 -0
  122. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/search.py +0 -0
  123. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/skills.py +0 -0
  124. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/status.py +0 -0
  125. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/supervisor.py +0 -0
  126. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/text.py +0 -0
  127. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/recipes/tool_loop.py +0 -0
  128. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/redaction.py +0 -0
  129. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/relations.py +0 -0
  130. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/replay.py +0 -0
  131. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/request.py +0 -0
  132. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/resources.py +0 -0
  133. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/routing.py +0 -0
  134. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/scheduler.py +0 -0
  135. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/session.py +0 -0
  136. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact/SKILL.md +0 -0
  137. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  138. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  139. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  140. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  141. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  142. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  143. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  144. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  145. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  146. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  147. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  148. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  149. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  150. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  151. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  152. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/sources.py +0 -0
  153. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/streaming.py +0 -0
  154. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/structured.py +0 -0
  155. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/__init__.py +0 -0
  156. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/assertions.py +0 -0
  157. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/exceptions.py +0 -0
  158. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/fault.py +0 -0
  159. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/golden.py +0 -0
  160. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/lab.py +0 -0
  161. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/mock.py +0 -0
  162. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/pytest_plugin.py +0 -0
  163. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/record.py +0 -0
  164. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/testing/registry.py +0 -0
  165. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tool_use.py +0 -0
  166. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tools.py +0 -0
  167. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/_otlp.py +0 -0
  168. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/columns.py +0 -0
  169. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/templates/sessions.html +0 -0
  170. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/templates/ui.html +0 -0
  171. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/tracing/web.py +0 -0
  172. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/triggers.py +0 -0
  173. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/types.py +0 -0
  174. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/verify.py +0 -0
  175. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/viz.py +0 -0
  176. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact/web.py +0 -0
  177. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact.egg-info/SOURCES.txt +0 -0
  178. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact.egg-info/dependency_links.txt +0 -0
  179. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact.egg-info/entry_points.txt +0 -0
  180. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact.egg-info/requires.txt +0 -0
  181. {reactifact-0.15.1 → reactifact-0.15.2}/reactifact.egg-info/top_level.txt +0 -0
  182. {reactifact-0.15.1 → reactifact-0.15.2}/setup.cfg +0 -0
  183. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_a2a.py +0 -0
  184. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_a2a_example.py +0 -0
  185. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_a2a_interop.py +0 -0
  186. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_absent_consume.py +0 -0
  187. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_adaptive.py +0 -0
  188. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_agent_tool.py +0 -0
  189. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_anthropic_provider.py +0 -0
  190. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_artifacts.py +0 -0
  191. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_audit.py +0 -0
  192. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_authz.py +0 -0
  193. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_backbone.py +0 -0
  194. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_branching.py +0 -0
  195. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_budget.py +0 -0
  196. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_cache.py +0 -0
  197. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_canonical_ports.py +0 -0
  198. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_chat_example.py +0 -0
  199. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_chat_memory.py +0 -0
  200. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_chat_outbox.py +0 -0
  201. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_chat_web.py +0 -0
  202. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_checkpoint.py +0 -0
  203. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_checkpoints_concurrency.py +0 -0
  204. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_cli.py +0 -0
  205. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_cli_scenario.py +0 -0
  206. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_commit_log.py +0 -0
  207. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_compaction.py +0 -0
  208. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_concurrency.py +0 -0
  209. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_consumes_produces.py +0 -0
  210. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_context_builder.py +0 -0
  211. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_correlated_consume.py +0 -0
  212. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_debounce.py +0 -0
  213. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_determinism.py +0 -0
  214. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_devops.py +0 -0
  215. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_devops_online.py +0 -0
  216. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_devops_web.py +0 -0
  217. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_effects.py +0 -0
  218. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_eval.py +0 -0
  219. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_eval_dataset.py +0 -0
  220. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_eval_judge.py +0 -0
  221. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_eval_online.py +0 -0
  222. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_eval_trajectory.py +0 -0
  223. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_fintech_audit.py +0 -0
  224. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_forklab.py +0 -0
  225. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_forklab_web.py +0 -0
  226. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_friendly_api.py +0 -0
  227. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_gemini_provider.py +0 -0
  228. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_golden.py +0 -0
  229. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_guardrails.py +0 -0
  230. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_hitl.py +0 -0
  231. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_image_provider.py +0 -0
  232. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_incident_commander.py +0 -0
  233. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_invalidation.py +0 -0
  234. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_join_consume.py +0 -0
  235. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_knowledge.py +0 -0
  236. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_knowledge_web.py +0 -0
  237. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_llm_ladder.py +0 -0
  238. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_logging.py +0 -0
  239. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_loop_bound_client.py +0 -0
  240. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_mcp.py +0 -0
  241. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_medic_lab.py +0 -0
  242. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_medic_lab_web.py +0 -0
  243. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_metrics.py +0 -0
  244. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_multisource.py +0 -0
  245. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_native_tool_use.py +0 -0
  246. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_openai_provider.py +0 -0
  247. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_outbox.py +0 -0
  248. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_patches.py +0 -0
  249. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_pending_actions.py +0 -0
  250. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_ports.py +0 -0
  251. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_produce_trigger.py +0 -0
  252. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_prompts.py +0 -0
  253. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_provider_auth.py +0 -0
  254. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_provider_retry.py +0 -0
  255. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_providers_integration.py +0 -0
  256. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_quick.py +0 -0
  257. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_quota.py +0 -0
  258. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_reacts_to.py +0 -0
  259. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipe_conversation.py +0 -0
  260. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipe_plan_execute.py +0 -0
  261. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipe_reflection.py +0 -0
  262. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipe_supervisor.py +0 -0
  263. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipe_tool_loop.py +0 -0
  264. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipes.py +0 -0
  265. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipes_inputs.py +0 -0
  266. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipes_memory.py +0 -0
  267. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_recipes_skills.py +0 -0
  268. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_redaction.py +0 -0
  269. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_relation_graph.py +0 -0
  270. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_relations.py +0 -0
  271. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_repair.py +0 -0
  272. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_repair_web.py +0 -0
  273. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_replay.py +0 -0
  274. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_repo_agent.py +0 -0
  275. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_request.py +0 -0
  276. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_research.py +0 -0
  277. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_resources.py +0 -0
  278. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_retry.py +0 -0
  279. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_routing.py +0 -0
  280. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_runtime.py +0 -0
  281. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_runtime_errors.py +0 -0
  282. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_scheduler_semantics.py +0 -0
  283. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_sessions.py +0 -0
  284. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_shutdown.py +0 -0
  285. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_skills.py +0 -0
  286. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_sources.py +0 -0
  287. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_sources_search.py +0 -0
  288. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_sources_vector.py +0 -0
  289. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_speech_provider.py +0 -0
  290. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_starter_app.py +0 -0
  291. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_streaming.py +0 -0
  292. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_structured.py +0 -0
  293. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_support_copilot.py +0 -0
  294. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_assertions.py +0 -0
  295. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_golden.py +0 -0
  296. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_lab.py +0 -0
  297. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_mock.py +0 -0
  298. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_provenance.py +0 -0
  299. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_pytest_plugin.py +0 -0
  300. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_testing_registry.py +0 -0
  301. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_token_budget.py +0 -0
  302. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_tools.py +0 -0
  303. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_trace_columns.py +0 -0
  304. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_trace_tags.py +0 -0
  305. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_type_registry.py +0 -0
  306. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_vendor_factories.py +0 -0
  307. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_vendor_multimodal_factories.py +0 -0
  308. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_verify.py +0 -0
  309. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_video_provider.py +0 -0
  310. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_view.py +0 -0
  311. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_viz.py +0 -0
  312. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_web_source.py +0 -0
  313. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_workspace.py +0 -0
  314. {reactifact-0.15.1 → reactifact-0.15.2}/tests/test_workspace_with_sources.py +0 -0
  315. {reactifact-0.15.1 → reactifact-0.15.2}/tests/tests_checkpoints_sqlite.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.15.1
3
+ Version: 0.15.2
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
@@ -380,7 +380,7 @@ uv sync --extra dev --extra web
380
380
 
381
381
  ## Status & contributing
382
382
 
383
- Active, `0.14.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
383
+ Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
384
384
  (see the [roadmap](docs/roadmap.md)); releases follow
385
385
  [Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
386
386
  [GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
@@ -334,7 +334,7 @@ uv sync --extra dev --extra web
334
334
 
335
335
  ## Status & contributing
336
336
 
337
- Active, `0.14.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
337
+ Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
338
338
  (see the [roadmap](docs/roadmap.md)); releases follow
339
339
  [Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
340
340
  [GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.15.1"
7
+ version = "0.15.2"
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, ToolOutput, tool
51
51
  from .triggers import Trigger
52
52
  from .types import register as register_type
53
53
 
54
- __version__ = "0.15.1"
54
+ __version__ = "0.15.2"
55
55
 
56
56
  __all__ = [
57
57
  "Agent",
@@ -1,17 +1,42 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import time
3
4
  from abc import ABC
4
5
  from collections.abc import Sequence
6
+ from contextvars import ContextVar, Token
5
7
  from typing import Any
6
8
 
7
9
  from .artifacts import Artifact
8
10
  from .consume import Consume
9
11
  from .context import Context
12
+ from .effects import current_effects
10
13
  from .events import Event, EventType
11
14
  from .patches import Patch
12
15
  from .produce import Produce, ProduceCall
13
16
  from .triggers import Trigger
14
17
 
18
+ #: Per-task log of the produces that ran for the current agent execution:
19
+ #: ``(produce_class_name, operations_authored, latency_ms)``. ``Runtime._execute``
20
+ #: opens it (and later records the span), ``Agent.execute`` appends — a
21
+ #: contextvar so concurrent executions sharing one ``Agent`` instance never race.
22
+ _produce_runs: ContextVar[list[tuple[str, int, float]] | None] = ContextVar(
23
+ "reactifact_produce_runs", default=None
24
+ )
25
+
26
+
27
+ def begin_produce_runs() -> Token[Any]:
28
+ """Opens the per-task produce-run log (see ``_produce_runs``)."""
29
+ return _produce_runs.set([])
30
+
31
+
32
+ def produce_runs() -> list[tuple[str, int, float]]:
33
+ """The produce runs recorded for the current task (empty if none)."""
34
+ return _produce_runs.get() or []
35
+
36
+
37
+ def end_produce_runs(token: Token[Any]) -> None:
38
+ _produce_runs.reset(token)
39
+
15
40
 
16
41
  class Agent(ABC): # noqa: B024 — interface without abstract methods, run() has a default
17
42
  """Base container: consumes some artifacts and produces others.
@@ -154,14 +179,30 @@ class Agent(ABC): # noqa: B024 — interface without abstract methods, run() ha
154
179
  if not self.produces:
155
180
  return None
156
181
  inputs = self._collect_inputs(context)
182
+ slot = current_effects()
183
+ log = _produce_runs.get()
157
184
  for p in self.produces:
158
185
  runs, trigger = self._resolve_produce_call(p, event, context)
159
186
  if not runs:
160
187
  continue
188
+ before = len(slot.operations) if slot is not None else 0
189
+ started = time.monotonic()
161
190
  call = ProduceCall(
162
191
  context=context, inputs=inputs, event=event, trigger=trigger
163
192
  )
164
- await p.produce(call)
193
+ try:
194
+ await p.produce(call)
195
+ finally:
196
+ # Record the produce even when it raises — a failing produce is
197
+ # exactly what a trace should show (the span is then the error
198
+ # span, with this produce's partial operations/latency).
199
+ if log is not None:
200
+ operations = (
201
+ len(slot.operations) if slot is not None else 0
202
+ ) - before
203
+ log.append(
204
+ (p.name, operations, (time.monotonic() - started) * 1000)
205
+ )
165
206
  return None
166
207
 
167
208
  @staticmethod
@@ -266,6 +266,37 @@ class Produce(Generic[TOut]):
266
266
  """
267
267
  return None
268
268
 
269
+ #: Explicit display name (set by e.g. tool-use produces); when unset,
270
+ #: `name` derives a readable one from the class/function/artifact type.
271
+ _name: str | None = None
272
+
273
+ @property
274
+ def name(self) -> str:
275
+ """A human-readable name for traces/logs.
276
+
277
+ Subclass-style produces use their class name; an ``@produce``-decorated
278
+ function uses the wrapped function's name; a bare ``Produce(Model)``
279
+ (declared only to widen an agent's allowed ``Create`` types) falls back
280
+ to its output artifact type — otherwise every such no-op would show up
281
+ as a generic ``Produce``. An explicit ``name`` (e.g. a tool-use produce's)
282
+ always wins.
283
+ """
284
+ explicit = getattr(self, "_name", None)
285
+ if explicit:
286
+ return str(explicit)
287
+ cls = type(self).__name__
288
+ if cls not in ("Produce", "_FunctionProduce"):
289
+ return cls
290
+ func = getattr(self, "_func", None)
291
+ if func is not None:
292
+ return str(getattr(func, "__name__", cls))
293
+ artifact = getattr(self.artifact_type, "__name__", "")
294
+ return f"Produce[{artifact}]" if artifact else cls
295
+
296
+ @name.setter
297
+ def name(self, value: str) -> None:
298
+ self._name = value
299
+
269
300
  def _apply_result(self, result: Any) -> None:
270
301
  """Writes a `@produce`-decorated function's return-style result into
271
302
  the effect slot (§24).
@@ -319,6 +350,8 @@ def produce(
319
350
  )
320
351
 
321
352
  class _FunctionProduce(Produce[Any]):
353
+ _func: Callable[[ProduceCall], Any]
354
+
322
355
  async def produce(self, call: ProduceCall) -> None:
323
356
  result = func(call)
324
357
  if asyncio.iscoroutine(result):
@@ -329,6 +362,8 @@ def produce(
329
362
  instance = _FunctionProduce(
330
363
  artifact_type=artifact_type, also_creates=also_creates, reacts_to=reacts_to
331
364
  )
365
+ # Keep the function for `Produce.name` (a readable trace/log label).
366
+ instance._func = func
332
367
  return instance
333
368
 
334
369
  return decorator
@@ -7,7 +7,7 @@ from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
7
7
  from dataclasses import dataclass
8
8
  from typing import Any, Literal, cast
9
9
 
10
- from .agents import Agent
10
+ from .agents import Agent, begin_produce_runs, end_produce_runs, produce_runs
11
11
  from .artifacts import Artifact
12
12
  from .budget import Budget, BudgetLLM, BudgetTracker, RunOutcome, RunStats
13
13
  from .commit import Commit, Read, Write
@@ -31,9 +31,17 @@ logger = get_logger(__name__)
31
31
  #: A scheduled patch with its trigger reads and (optional) trace span.
32
32
  PatchWork = tuple[Patch, Agent, list[Read], AgentSpan | None]
33
33
  #: One agent execution result: patch (if any), the agent/event/reads that
34
- #: produced it, latency, and the exception if the produce raised and
35
- #: `isolate_errors` was set.
36
- AgentResult = tuple[Patch | None, Agent, Event, list[Read], float, BaseException | None]
34
+ #: produced it, latency, the exception if the produce raised and
35
+ #: `isolate_errors` was set, and the produces that ran (name/ops/latency).
36
+ AgentResult = tuple[
37
+ Patch | None,
38
+ Agent,
39
+ Event,
40
+ list[Read],
41
+ float,
42
+ BaseException | None,
43
+ list[tuple[str, int, float]],
44
+ ]
37
45
  #: Performs one committed `PendingAction` (outbox). Called by the runtime after
38
46
  #: the intent is committed; the app performs the real I/O and returns (or
39
47
  #: raises). Must be idempotent on `idempotency_key` (§42).
@@ -650,18 +658,22 @@ class Runtime:
650
658
  """
651
659
  patches_to_apply: list[PatchWork] = []
652
660
  runs = 0
653
- for patch, agent, event, reads, latency, error in results:
661
+ for patch, agent, event, reads, latency, error, produce_run_list in results:
654
662
  if self._budget_exhausted():
655
663
  break
656
664
  runs += 1
657
665
  self._runs_used += 1
658
666
  if error is not None:
659
667
  self._errors_used += 1
660
- self._trace.record_span(agent, event, reads, latency, error=error)
668
+ self._trace.record_span(
669
+ agent, event, reads, latency, error=error, produces=produce_run_list
670
+ )
661
671
  continue
662
672
  if patch is None or patch.is_empty():
663
673
  continue
664
- span = self._trace.record_span(agent, event, reads, latency)
674
+ span = self._trace.record_span(
675
+ agent, event, reads, latency, produces=produce_run_list
676
+ )
665
677
  self._validate_patch_types(patch, agent)
666
678
  self._enforce_authorization(patch)
667
679
  patch = self._apply_guardrails(patch)
@@ -794,6 +806,7 @@ class Runtime:
794
806
  task = asyncio.current_task()
795
807
  self._trace.register_task(task, agent.name)
796
808
  effects_token = set_effects(Effects(self.context))
809
+ produce_runs_token = begin_produce_runs()
797
810
  slot: Effects | None = None
798
811
  patch: Patch | None = None
799
812
  error: BaseException | None = None
@@ -817,6 +830,8 @@ class Runtime:
817
830
  if self.on_agent_error is not None:
818
831
  self.on_agent_error(agent, event, exc)
819
832
  finally:
833
+ runs = produce_runs()
834
+ end_produce_runs(produce_runs_token)
820
835
  reset_effects(effects_token)
821
836
  self._trace.unregister_task(task)
822
837
  latency = (time.monotonic() - started) * 1000
@@ -829,7 +844,7 @@ class Runtime:
829
844
  "error": repr(error),
830
845
  },
831
846
  )
832
- return None, agent, event, reads, latency, error
847
+ return None, agent, event, reads, latency, error, runs
833
848
  logger.debug(
834
849
  "agent finished",
835
850
  extra={"agent": agent.name, "latency_ms": round(latency, 2)},
@@ -843,7 +858,7 @@ class Runtime:
843
858
  combined.merge(patch)
844
859
  combined.merge(slot.to_patch())
845
860
  patch = combined
846
- return patch, agent, event, reads, latency, None
861
+ return patch, agent, event, reads, latency, None, runs
847
862
 
848
863
  async def arun(
849
864
  self,
@@ -11,6 +11,7 @@ from .models import (
11
11
  AgentSpan,
12
12
  ArtifactRef,
13
13
  LLMCall,
14
+ ProduceRun,
14
15
  RelationRef,
15
16
  RunTrace,
16
17
  Tag,
@@ -35,6 +36,7 @@ __all__ = [
35
36
  "LangfuseTracer",
36
37
  "OTLPTracer",
37
38
  "PostgresStore",
39
+ "ProduceRun",
38
40
  "RecordingLLM",
39
41
  "RelationRef",
40
42
  "RunTrace",
@@ -96,6 +96,11 @@ class LangfuseTracer(Tracer):
96
96
  )
97
97
  span_start = span.started_at or run_start
98
98
  agent_span_id = _span_id(f"{trace_id}:span:{i}:{span.agent}")
99
+ spans.extend(
100
+ self._produce_spans(
101
+ trace_id, agent_span_id, trace_attrs, run_start, i, span
102
+ )
103
+ )
99
104
  for j, call in enumerate(span.llm_calls):
100
105
  spans.append(
101
106
  self._llm_span(
@@ -145,8 +150,29 @@ class LangfuseTracer(Tracer):
145
150
  _type_summary(span.writes),
146
151
  ),
147
152
  ]
153
+ if span.event_artifact_type:
154
+ attrs.append(
155
+ _attr(
156
+ "langfuse.observation.metadata.event_artifact_type",
157
+ span.event_artifact_type,
158
+ )
159
+ )
148
160
  if span.error:
149
161
  attrs.append(_attr("langfuse.observation.metadata.error", span.error))
162
+ if span.produces:
163
+ attrs.append(
164
+ _attr(
165
+ "langfuse.observation.metadata.produces",
166
+ [
167
+ {
168
+ "name": p.name,
169
+ "operations": p.operations,
170
+ "latency_ms": p.latency_ms,
171
+ }
172
+ for p in span.produces
173
+ ],
174
+ )
175
+ )
150
176
  return {
151
177
  "traceId": trace_id,
152
178
  "spanId": _span_id(f"{trace_id}:span:{index}:{span.agent}"),
@@ -158,6 +184,53 @@ class LangfuseTracer(Tracer):
158
184
  "attributes": attrs,
159
185
  }
160
186
 
187
+ def _produce_spans(
188
+ self,
189
+ trace_id: str,
190
+ parent_span_id: str,
191
+ trace_attrs: list[dict[str, Any]],
192
+ run_start: datetime,
193
+ span_index: int,
194
+ span: AgentSpan,
195
+ ) -> list[dict[str, Any]]:
196
+ """One child observation per produce the agent ran, so Langfuse's
197
+ waterfall shows the ``Consume → Produce`` steps instead of a single
198
+ opaque agent span.
199
+
200
+ Produces run sequentially within an agent, so they are laid out
201
+ back-to-back from the span start by each one's measured latency.
202
+ """
203
+ if not span.produces:
204
+ return []
205
+ cursor = span.started_at or run_start
206
+ out: list[dict[str, Any]] = []
207
+ for k, produce in enumerate(span.produces):
208
+ end = cursor + timedelta(milliseconds=produce.latency_ms)
209
+ out.append(
210
+ {
211
+ "traceId": trace_id,
212
+ "spanId": _span_id(
213
+ f"{trace_id}:produce:{span_index}:{k}:{produce.name}"
214
+ ),
215
+ "parentSpanId": parent_span_id,
216
+ "name": produce.name,
217
+ "kind": 1,
218
+ "startTimeUnixNano": _unix_nanos(cursor),
219
+ "endTimeUnixNano": _unix_nanos(end),
220
+ "attributes": [
221
+ *trace_attrs,
222
+ _attr("langfuse.observation.type", "span"),
223
+ _attr("langfuse.observation.metadata.agent", span.agent),
224
+ _attr(
225
+ "langfuse.observation.metadata.operations",
226
+ produce.operations,
227
+ ),
228
+ ],
229
+ }
230
+ )
231
+ cursor = end
232
+ return out
233
+
161
234
  def _llm_span(
162
235
  self,
163
236
  trace_id: str,
@@ -88,11 +88,31 @@ class TagAssignment(BaseModel):
88
88
  updated_at: datetime | None = None
89
89
 
90
90
 
91
+ class ProduceRun(BaseModel):
92
+ """One produce execution within an agent span (§54).
93
+
94
+ An agent runs one or more ``Produce``s per event; the span records which
95
+ ones actually ran and how much each contributed (``operations`` = authored
96
+ effect operations, so the produce that did the work is distinguishable from
97
+ one that merely matched the event).
98
+ """
99
+
100
+ name: str
101
+ operations: int = 0
102
+ latency_ms: float = 0.0
103
+
104
+
91
105
  class AgentSpan(BaseModel):
92
106
  """A single agent execution within a run (§54)."""
93
107
 
94
108
  agent: str
95
109
  event_type: str = ""
110
+ #: The artifact type whose change triggered this agent (its matched
111
+ #: ``Consume``) — e.g. ``DraftAnswer`` vs ``FinalResponse`` for an agent that
112
+ #: consumes both. ``event_type`` alone (created/updated) can't tell them apart.
113
+ event_artifact_type: str = ""
114
+ #: The produces that ran for this event, in execution order.
115
+ produces: list[ProduceRun] = Field(default_factory=list)
96
116
  reads: list[ArtifactRef] = Field(default_factory=list)
97
117
  writes: list[ArtifactRef] = Field(default_factory=list)
98
118
  relations: list[RelationRef] = Field(default_factory=list)
@@ -123,8 +123,19 @@ class OTLPTracer(Tracer):
123
123
  attr("reactifact.reads", type_summary(span.reads)),
124
124
  attr("reactifact.writes", type_summary(span.writes)),
125
125
  ]
126
+ if span.event_artifact_type:
127
+ attrs.append(
128
+ attr("reactifact.event_artifact_type", span.event_artifact_type)
129
+ )
126
130
  if span.error:
127
131
  attrs.append(attr("error.type", span.error))
132
+ if span.produces:
133
+ attrs.append(
134
+ attr(
135
+ "reactifact.produces",
136
+ ", ".join(f"{p.name}({p.operations})" for p in span.produces),
137
+ )
138
+ )
128
139
  return {
129
140
  "traceId": tid,
130
141
  "spanId": span_id(f"{tid}:span:{index}:{span.agent}"),
@@ -27,6 +27,7 @@ from .models import (
27
27
  AgentSpan,
28
28
  ArtifactRef,
29
29
  LLMCall,
30
+ ProduceRun,
30
31
  RelationRef,
31
32
  RunTrace,
32
33
  TagAssignment,
@@ -70,13 +71,15 @@ class PostgresStore:
70
71
  run_id TEXT NOT NULL REFERENCES runs(id),
71
72
  agent TEXT NOT NULL,
72
73
  event_type TEXT NOT NULL DEFAULT '',
74
+ event_artifact_type TEXT NOT NULL DEFAULT '',
73
75
  latency_ms REAL NOT NULL DEFAULT 0,
74
76
  error TEXT,
75
77
  started_at TIMESTAMPTZ,
76
78
  reads JSONB NOT NULL DEFAULT '[]',
77
79
  writes JSONB NOT NULL DEFAULT '[]',
78
80
  relations JSONB NOT NULL DEFAULT '[]',
79
- llm_calls JSONB NOT NULL DEFAULT '[]'
81
+ llm_calls JSONB NOT NULL DEFAULT '[]',
82
+ produces JSONB NOT NULL DEFAULT '[]'
80
83
  )
81
84
  """
82
85
  )
@@ -122,6 +125,14 @@ class PostgresStore:
122
125
  await cur.execute(
123
126
  "ALTER TABLE spans ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ"
124
127
  )
128
+ await cur.execute(
129
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
130
+ "produces JSONB NOT NULL DEFAULT '[]'"
131
+ )
132
+ await cur.execute(
133
+ "ALTER TABLE spans ADD COLUMN IF NOT EXISTS "
134
+ "event_artifact_type TEXT NOT NULL DEFAULT ''"
135
+ )
125
136
  await conn.commit()
126
137
  finally:
127
138
  await conn.close()
@@ -159,13 +170,15 @@ class PostgresStore:
159
170
  if span_started is not None and span_started.tzinfo is None:
160
171
  span_started = span_started.replace(tzinfo=UTC)
161
172
  await cur.execute(
162
- "INSERT INTO spans (run_id, agent, event_type, latency_ms, error, "
163
- "started_at, reads, writes, relations, llm_calls) "
164
- "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
173
+ "INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
174
+ "latency_ms, error, started_at, reads, writes, relations, llm_calls, "
175
+ "produces) "
176
+ "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
165
177
  (
166
178
  trace.id,
167
179
  span.agent,
168
180
  span.event_type,
181
+ span.event_artifact_type,
169
182
  span.latency_ms,
170
183
  span.error,
171
184
  span_started,
@@ -181,6 +194,9 @@ class PostgresStore:
181
194
  psycopg.types.json.Jsonb(
182
195
  [c.model_dump(mode="json") for c in span.llm_calls]
183
196
  ),
197
+ psycopg.types.json.Jsonb(
198
+ [p.model_dump(mode="json") for p in span.produces]
199
+ ),
184
200
  ),
185
201
  )
186
202
  await conn.commit()
@@ -387,8 +403,8 @@ class PostgresStore:
387
403
  if row is None:
388
404
  return None
389
405
  await cur.execute(
390
- "SELECT agent, event_type, latency_ms, error, started_at, "
391
- "reads, writes, relations, llm_calls "
406
+ "SELECT agent, event_type, event_artifact_type, latency_ms, error, "
407
+ "started_at, reads, writes, relations, llm_calls, produces "
392
408
  "FROM spans WHERE run_id = %s ORDER BY id",
393
409
  (trace_id,),
394
410
  )
@@ -408,13 +424,15 @@ class PostgresStore:
408
424
  AgentSpan(
409
425
  agent=r[0],
410
426
  event_type=r[1],
411
- latency_ms=r[2],
412
- error=r[3],
413
- started_at=r[4],
414
- reads=[ArtifactRef(**d) for d in r[5]],
415
- writes=[ArtifactRef(**d) for d in r[6]],
416
- relations=[RelationRef(**d) for d in r[7]],
417
- llm_calls=[LLMCall(**d) for d in r[8]],
427
+ event_artifact_type=r[2],
428
+ latency_ms=r[3],
429
+ error=r[4],
430
+ started_at=r[5],
431
+ reads=[ArtifactRef(**d) for d in r[6]],
432
+ writes=[ArtifactRef(**d) for d in r[7]],
433
+ relations=[RelationRef(**d) for d in r[8]],
434
+ llm_calls=[LLMCall(**d) for d in r[9]],
435
+ produces=[ProduceRun(**d) for d in r[10]],
418
436
  )
419
437
  for r in span_rows
420
438
  ]
@@ -30,6 +30,7 @@ from .models import (
30
30
  AgentSpan,
31
31
  ArtifactRef,
32
32
  LLMCall,
33
+ ProduceRun,
33
34
  RelationRef,
34
35
  RunTrace,
35
36
  TagAssignment,
@@ -174,13 +175,15 @@ class TraceStore:
174
175
  run_id TEXT NOT NULL REFERENCES runs(id),
175
176
  agent TEXT NOT NULL,
176
177
  event_type TEXT NOT NULL DEFAULT '',
178
+ event_artifact_type TEXT NOT NULL DEFAULT '',
177
179
  latency_ms REAL NOT NULL DEFAULT 0,
178
180
  error TEXT,
179
181
  started_at REAL,
180
182
  reads TEXT NOT NULL DEFAULT '[]',
181
183
  writes TEXT NOT NULL DEFAULT '[]',
182
184
  relations TEXT NOT NULL DEFAULT '[]',
183
- llm_calls TEXT NOT NULL DEFAULT '[]'
185
+ llm_calls TEXT NOT NULL DEFAULT '[]',
186
+ produces TEXT NOT NULL DEFAULT '[]'
184
187
  );
185
188
  CREATE TABLE IF NOT EXISTS tags (
186
189
  name TEXT PRIMARY KEY,
@@ -218,6 +221,14 @@ class TraceStore:
218
221
  )
219
222
  if "started_at" not in cols:
220
223
  self._conn.execute("ALTER TABLE spans ADD COLUMN started_at REAL")
224
+ if "produces" not in cols:
225
+ self._conn.execute(
226
+ "ALTER TABLE spans ADD COLUMN produces TEXT NOT NULL DEFAULT '[]'"
227
+ )
228
+ if "event_artifact_type" not in cols:
229
+ self._conn.execute(
230
+ "ALTER TABLE spans ADD COLUMN event_artifact_type TEXT NOT NULL DEFAULT ''"
231
+ )
221
232
  run_cols = {row[1] for row in self._conn.execute("PRAGMA table_info(runs)")}
222
233
  if "prompt_tokens" not in run_cols:
223
234
  self._conn.execute(
@@ -328,13 +339,15 @@ class TraceStore:
328
339
  )
329
340
  for span in trace.spans:
330
341
  self._conn.execute(
331
- "INSERT INTO spans (run_id, agent, event_type, latency_ms, error, "
332
- "started_at, reads, writes, relations, llm_calls) "
333
- "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
342
+ "INSERT INTO spans (run_id, agent, event_type, event_artifact_type, "
343
+ "latency_ms, error, started_at, reads, writes, relations, llm_calls, "
344
+ "produces) "
345
+ "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
334
346
  (
335
347
  trace.id,
336
348
  span.agent,
337
349
  span.event_type,
350
+ span.event_artifact_type,
338
351
  span.latency_ms,
339
352
  span.error,
340
353
  span.started_at.timestamp() if span.started_at else None,
@@ -342,6 +355,7 @@ class TraceStore:
342
355
  json.dumps([w.model_dump(mode="json") for w in span.writes]),
343
356
  json.dumps([r.model_dump(mode="json") for r in span.relations]),
344
357
  json.dumps([c.model_dump(mode="json") for c in span.llm_calls]),
358
+ json.dumps([p.model_dump(mode="json") for p in span.produces]),
345
359
  ),
346
360
  )
347
361
  self._conn.commit()
@@ -595,8 +609,8 @@ class TraceStore:
595
609
  if row is None:
596
610
  return None
597
611
  span_rows = self._conn.execute(
598
- "SELECT agent, event_type, latency_ms, error, started_at, "
599
- "reads, writes, relations, llm_calls "
612
+ "SELECT agent, event_type, event_artifact_type, latency_ms, error, started_at, "
613
+ "reads, writes, relations, llm_calls, produces "
600
614
  "FROM spans WHERE run_id = ? ORDER BY id",
601
615
  (trace_id,),
602
616
  ).fetchall()
@@ -604,15 +618,17 @@ class TraceStore:
604
618
  AgentSpan(
605
619
  agent=r[0],
606
620
  event_type=r[1],
607
- latency_ms=r[2],
608
- error=r[3],
621
+ event_artifact_type=r[2],
622
+ latency_ms=r[3],
623
+ error=r[4],
609
624
  started_at=(
610
- datetime.fromtimestamp(r[4], tz=UTC) if r[4] is not None else None
625
+ datetime.fromtimestamp(r[5], tz=UTC) if r[5] is not None else None
611
626
  ),
612
- reads=[ArtifactRef(**d) for d in json.loads(r[5])],
613
- writes=[ArtifactRef(**d) for d in json.loads(r[6])],
614
- relations=[RelationRef(**d) for d in json.loads(r[7])],
615
- llm_calls=[LLMCall(**d) for d in json.loads(r[8])],
627
+ reads=[ArtifactRef(**d) for d in json.loads(r[6])],
628
+ writes=[ArtifactRef(**d) for d in json.loads(r[7])],
629
+ relations=[RelationRef(**d) for d in json.loads(r[8])],
630
+ llm_calls=[LLMCall(**d) for d in json.loads(r[9])],
631
+ produces=[ProduceRun(**d) for d in json.loads(r[10])],
616
632
  )
617
633
  for r in span_rows
618
634
  ]
@@ -258,11 +258,22 @@ tbody tr.row.selected td{background:var(--accent-soft);}
258
258
  details.agent{border:1px solid var(--border);border-radius:var(--r-md);background:var(--panel);overflow:hidden;}
259
259
  details.agent>summary{display:flex;align-items:center;gap:12px;padding:12px 16px;cursor:pointer;font-size:14px;}
260
260
  details.agent>summary:hover{background:var(--hover);}
261
- .caret{color:var(--muted);transition:transform .15s;} details[open]>.caret,.caret.open{transform:rotate(90deg);}
261
+ .caret{color:var(--muted);transition:transform .15s;} details[open]>summary .caret,.caret.open{transform:rotate(90deg);}
262
262
  .agent .muted,.spans-stat{color:var(--muted);font-size:12.5px;font-variant-numeric:tabular-nums;}
263
263
  .agent-body{padding:6px 16px 16px;border-top:1px solid var(--border);background:var(--panel-alt);}
264
264
  .chips{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0;}
265
265
  .chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-soft);color:var(--accent);border-radius:12px;padding:2px 9px;font-family:var(--mono);font-size:11.5px;}
266
+ .flow{display:inline-flex;align-items:center;gap:7px;min-width:0;flex-wrap:wrap;}
267
+ .flow .c-side{display:inline-flex;align-items:center;gap:5px;min-width:0;}
268
+ .flow .consume{font-family:var(--mono);font-size:11.5px;color:var(--chip-text);background:var(--chip);border-radius:6px;padding:1px 7px;white-space:nowrap;}
269
+ .flow .consume.muted{color:var(--muted);background:transparent;border:1px dashed var(--border-strong);}
270
+ .flow .ev{font-family:var(--mono);font-size:10.5px;color:var(--faint);white-space:nowrap;}
271
+ .flow .arrow{color:var(--muted);font-family:var(--mono);}
272
+ .produces{display:inline-flex;flex-wrap:wrap;gap:5px;min-width:0;}
273
+ .chip.produce{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11.5px;background:var(--ok-soft);color:var(--ok);border-radius:8px;padding:1px 4px 1px 8px;white-space:nowrap;}
274
+ .chip.produce.noop{background:var(--panel-alt);color:var(--muted);border:1px solid var(--border);}
275
+ .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
+ .chip.produce.noop .ops.zero{background:var(--border);color:var(--faint);}
266
277
  .spans{display:flex;flex-direction:column;gap:8px;}
267
278
  details.node{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--r-sm);background:var(--panel);overflow:hidden;}
268
279
  details.node summary{display:flex;align-items:center;gap:10px;padding:9px 14px;cursor:pointer;font-size:13px;}