reactifact 0.15.1__tar.gz → 0.15.3__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.3}/PKG-INFO +19 -2
  2. {reactifact-0.15.1 → reactifact-0.15.3}/README.md +18 -1
  3. {reactifact-0.15.1 → reactifact-0.15.3}/pyproject.toml +1 -1
  4. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/__init__.py +1 -1
  5. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/agents.py +42 -1
  6. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/audit.py +22 -2
  7. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/interrupt.py +12 -1
  8. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/produce.py +35 -0
  9. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/runtime.py +24 -9
  10. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/__init__.py +2 -0
  11. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/langfuse.py +73 -0
  12. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/models.py +20 -0
  13. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/otlp.py +11 -0
  14. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/postgres.py +31 -13
  15. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/store.py +29 -13
  16. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/app.css +12 -1
  17. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/ui_run.html +30 -4
  18. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/tracer.py +13 -1
  19. {reactifact-0.15.1 → reactifact-0.15.3/reactifact.egg-info}/PKG-INFO +19 -2
  20. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_audit.py +33 -0
  21. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_outbox.py +10 -0
  22. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_produce_styles.py +17 -0
  23. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_public_api.py +1 -0
  24. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_tracing.py +117 -2
  25. {reactifact-0.15.1 → reactifact-0.15.3}/LICENSE +0 -0
  26. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/__main__.py +0 -0
  27. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/_extras.py +0 -0
  28. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/_httpx.py +0 -0
  29. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/__init__.py +0 -0
  30. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/client.py +0 -0
  31. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/models.py +0 -0
  32. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/remote.py +0 -0
  33. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/a2a/server.py +0 -0
  34. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/agent_tool.py +0 -0
  35. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/artifacts.py +0 -0
  36. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/authz.py +0 -0
  37. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/branching.py +0 -0
  38. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/budget.py +0 -0
  39. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cache.py +0 -0
  40. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/chat.py +0 -0
  41. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/checkpoints.py +0 -0
  42. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/__init__.py +0 -0
  43. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/branch.py +0 -0
  44. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/common.py +0 -0
  45. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/context.py +0 -0
  46. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/eval.py +0 -0
  47. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/graph.py +0 -0
  48. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/replay.py +0 -0
  49. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/scenario.py +0 -0
  50. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/skills.py +0 -0
  51. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/cli/trace.py +0 -0
  52. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/commit.py +0 -0
  53. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/commit_log.py +0 -0
  54. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/consume.py +0 -0
  55. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/context.py +0 -0
  56. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/context_builder.py +0 -0
  57. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/effects.py +0 -0
  58. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/__init__.py +0 -0
  59. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/dataset.py +0 -0
  60. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/evaluators.py +0 -0
  61. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/judge.py +0 -0
  62. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/online.py +0 -0
  63. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/runner.py +0 -0
  64. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/eval/scoring.py +0 -0
  65. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/events.py +0 -0
  66. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/guardrails.py +0 -0
  67. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/llm_agent.py +0 -0
  68. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/logging.py +0 -0
  69. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/__init__.py +0 -0
  70. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/client.py +0 -0
  71. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/oauth.py +0 -0
  72. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/mcp/server.py +0 -0
  73. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/metrics.py +0 -0
  74. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/native_tool_use.py +0 -0
  75. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/operations.py +0 -0
  76. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/patches.py +0 -0
  77. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/pricing.py +0 -0
  78. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/prompts.py +0 -0
  79. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/__init__.py +0 -0
  80. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/_retry.py +0 -0
  81. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/anthropic.py +0 -0
  82. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/azure.py +0 -0
  83. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/cerebras.py +0 -0
  84. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/chat.py +0 -0
  85. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/contracts.py +0 -0
  86. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/deepseek.py +0 -0
  87. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/fake.py +0 -0
  88. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/fireworks.py +0 -0
  89. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/gemini.py +0 -0
  90. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/github_models.py +0 -0
  91. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/groq.py +0 -0
  92. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/image.py +0 -0
  93. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/mistral.py +0 -0
  94. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/nvidia.py +0 -0
  95. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/ollama.py +0 -0
  96. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/openai.py +0 -0
  97. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/openrouter.py +0 -0
  98. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/perplexity.py +0 -0
  99. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/qwen.py +0 -0
  100. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/speech.py +0 -0
  101. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/together.py +0 -0
  102. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/video.py +0 -0
  103. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/xai.py +0 -0
  104. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/providers/zai.py +0 -0
  105. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/py.typed +0 -0
  106. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/__init__.py +0 -0
  107. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/_shared.py +0 -0
  108. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/agent.py +0 -0
  109. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/chat.py +0 -0
  110. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/models.py +0 -0
  111. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/rag.py +0 -0
  112. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quick/tools_agent.py +0 -0
  113. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/quota.py +0 -0
  114. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/__init__.py +0 -0
  115. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/cleanup.py +0 -0
  116. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/conversation.py +0 -0
  117. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/identity.py +0 -0
  118. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/inputs.py +0 -0
  119. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/memory.py +0 -0
  120. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/plan_execute.py +0 -0
  121. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/reflection.py +0 -0
  122. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/resolve.py +0 -0
  123. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/rollback.py +0 -0
  124. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/search.py +0 -0
  125. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/skills.py +0 -0
  126. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/status.py +0 -0
  127. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/supervisor.py +0 -0
  128. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/text.py +0 -0
  129. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/recipes/tool_loop.py +0 -0
  130. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/redaction.py +0 -0
  131. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/relations.py +0 -0
  132. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/replay.py +0 -0
  133. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/request.py +0 -0
  134. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/resources.py +0 -0
  135. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/routing.py +0 -0
  136. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/scheduler.py +0 -0
  137. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/session.py +0 -0
  138. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/SKILL.md +0 -0
  139. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  140. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  141. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  142. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  143. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  144. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  145. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  146. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  147. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  148. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  149. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  150. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  151. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  152. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  153. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  154. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/sources.py +0 -0
  155. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/streaming.py +0 -0
  156. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/structured.py +0 -0
  157. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/__init__.py +0 -0
  158. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/assertions.py +0 -0
  159. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/exceptions.py +0 -0
  160. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/fault.py +0 -0
  161. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/golden.py +0 -0
  162. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/lab.py +0 -0
  163. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/mock.py +0 -0
  164. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/pytest_plugin.py +0 -0
  165. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/record.py +0 -0
  166. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/testing/registry.py +0 -0
  167. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tool_use.py +0 -0
  168. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tools.py +0 -0
  169. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/_otlp.py +0 -0
  170. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/columns.py +0 -0
  171. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/sessions.html +0 -0
  172. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/templates/ui.html +0 -0
  173. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/tracing/web.py +0 -0
  174. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/triggers.py +0 -0
  175. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/types.py +0 -0
  176. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/verify.py +0 -0
  177. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/viz.py +0 -0
  178. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact/web.py +0 -0
  179. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/SOURCES.txt +0 -0
  180. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/dependency_links.txt +0 -0
  181. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/entry_points.txt +0 -0
  182. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/requires.txt +0 -0
  183. {reactifact-0.15.1 → reactifact-0.15.3}/reactifact.egg-info/top_level.txt +0 -0
  184. {reactifact-0.15.1 → reactifact-0.15.3}/setup.cfg +0 -0
  185. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a.py +0 -0
  186. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a_example.py +0 -0
  187. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_a2a_interop.py +0 -0
  188. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_absent_consume.py +0 -0
  189. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_adaptive.py +0 -0
  190. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_agent_tool.py +0 -0
  191. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_anthropic_provider.py +0 -0
  192. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_artifacts.py +0 -0
  193. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_authz.py +0 -0
  194. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_backbone.py +0 -0
  195. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_branching.py +0 -0
  196. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_budget.py +0 -0
  197. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cache.py +0 -0
  198. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_canonical_ports.py +0 -0
  199. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_example.py +0 -0
  200. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_memory.py +0 -0
  201. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_outbox.py +0 -0
  202. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_chat_web.py +0 -0
  203. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_checkpoint.py +0 -0
  204. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_checkpoints_concurrency.py +0 -0
  205. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cli.py +0 -0
  206. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_cli_scenario.py +0 -0
  207. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_commit_log.py +0 -0
  208. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_compaction.py +0 -0
  209. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_concurrency.py +0 -0
  210. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_consumes_produces.py +0 -0
  211. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_context_builder.py +0 -0
  212. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_correlated_consume.py +0 -0
  213. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_debounce.py +0 -0
  214. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_determinism.py +0 -0
  215. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops.py +0 -0
  216. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops_online.py +0 -0
  217. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_devops_web.py +0 -0
  218. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_effects.py +0 -0
  219. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval.py +0 -0
  220. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_dataset.py +0 -0
  221. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_judge.py +0 -0
  222. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_online.py +0 -0
  223. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_eval_trajectory.py +0 -0
  224. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_fintech_audit.py +0 -0
  225. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_forklab.py +0 -0
  226. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_forklab_web.py +0 -0
  227. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_friendly_api.py +0 -0
  228. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_gemini_provider.py +0 -0
  229. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_golden.py +0 -0
  230. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_guardrails.py +0 -0
  231. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_hitl.py +0 -0
  232. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_image_provider.py +0 -0
  233. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_incident_commander.py +0 -0
  234. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_invalidation.py +0 -0
  235. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_join_consume.py +0 -0
  236. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_knowledge.py +0 -0
  237. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_knowledge_web.py +0 -0
  238. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_llm_ladder.py +0 -0
  239. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_logging.py +0 -0
  240. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_loop_bound_client.py +0 -0
  241. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_mcp.py +0 -0
  242. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_medic_lab.py +0 -0
  243. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_medic_lab_web.py +0 -0
  244. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_metrics.py +0 -0
  245. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_multisource.py +0 -0
  246. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_native_tool_use.py +0 -0
  247. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_openai_provider.py +0 -0
  248. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_patches.py +0 -0
  249. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_pending_actions.py +0 -0
  250. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_ports.py +0 -0
  251. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_produce_trigger.py +0 -0
  252. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_prompts.py +0 -0
  253. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_provider_auth.py +0 -0
  254. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_provider_retry.py +0 -0
  255. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_providers_integration.py +0 -0
  256. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_quick.py +0 -0
  257. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_quota.py +0 -0
  258. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_reacts_to.py +0 -0
  259. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_conversation.py +0 -0
  260. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_plan_execute.py +0 -0
  261. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_reflection.py +0 -0
  262. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_supervisor.py +0 -0
  263. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipe_tool_loop.py +0 -0
  264. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes.py +0 -0
  265. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_inputs.py +0 -0
  266. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_memory.py +0 -0
  267. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_recipes_skills.py +0 -0
  268. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_redaction.py +0 -0
  269. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_relation_graph.py +0 -0
  270. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_relations.py +0 -0
  271. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repair.py +0 -0
  272. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repair_web.py +0 -0
  273. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_replay.py +0 -0
  274. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_repo_agent.py +0 -0
  275. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_request.py +0 -0
  276. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_research.py +0 -0
  277. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_resources.py +0 -0
  278. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_retry.py +0 -0
  279. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_routing.py +0 -0
  280. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_runtime.py +0 -0
  281. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_runtime_errors.py +0 -0
  282. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_scheduler_semantics.py +0 -0
  283. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sessions.py +0 -0
  284. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_shutdown.py +0 -0
  285. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_skills.py +0 -0
  286. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources.py +0 -0
  287. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources_search.py +0 -0
  288. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_sources_vector.py +0 -0
  289. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_speech_provider.py +0 -0
  290. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_starter_app.py +0 -0
  291. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_streaming.py +0 -0
  292. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_structured.py +0 -0
  293. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_support_copilot.py +0 -0
  294. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_assertions.py +0 -0
  295. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_golden.py +0 -0
  296. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_lab.py +0 -0
  297. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_mock.py +0 -0
  298. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_provenance.py +0 -0
  299. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_pytest_plugin.py +0 -0
  300. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_testing_registry.py +0 -0
  301. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_token_budget.py +0 -0
  302. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_tools.py +0 -0
  303. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_trace_columns.py +0 -0
  304. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_trace_tags.py +0 -0
  305. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_type_registry.py +0 -0
  306. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_vendor_factories.py +0 -0
  307. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_vendor_multimodal_factories.py +0 -0
  308. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_verify.py +0 -0
  309. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_video_provider.py +0 -0
  310. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_view.py +0 -0
  311. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_viz.py +0 -0
  312. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_web_source.py +0 -0
  313. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_workspace.py +0 -0
  314. {reactifact-0.15.1 → reactifact-0.15.3}/tests/test_workspace_with_sources.py +0 -0
  315. {reactifact-0.15.1 → reactifact-0.15.3}/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.3
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
@@ -323,6 +323,23 @@ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
323
323
  - **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
324
324
  `reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
325
325
 
326
+ ## Agent Skills
327
+
328
+ reactifact ships official **Agent Skills** — reusable instructions that teach a
329
+ coding agent (Claude Code, Cursor, Codex, Copilot, …) how to build *with*
330
+ reactifact. They're bundled with the package, so they always match your version:
331
+
332
+ ```bash
333
+ python -m reactifact skills list # what is available
334
+ python -m reactifact skills install # -> .agents/skills/
335
+ python -m reactifact skills install --target both # + .claude/skills/
336
+ ```
337
+
338
+ Skills: `reactifact` (mental model), `reactifact-agents`, `reactifact-llm`,
339
+ `reactifact-rag`, `reactifact-testing`, `reactifact-eval`,
340
+ `reactifact-observability`, `reactifact-from-langchain` — see
341
+ [Agent skills](docs/en/skills.md).
342
+
326
343
  ## Run a demo
327
344
 
328
345
  Offline-capable, no API keys required (deterministic fallbacks):
@@ -380,7 +397,7 @@ uv sync --extra dev --extra web
380
397
 
381
398
  ## Status & contributing
382
399
 
383
- Active, `0.14.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
400
+ Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
384
401
  (see the [roadmap](docs/roadmap.md)); releases follow
385
402
  [Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
386
403
  [GitHub Discussions](https://github.com/bzdvdn/reactifact/discussions); a star,
@@ -277,6 +277,23 @@ by hand. Run it yourself: `uv run python -m examples.ledger.main`.
277
277
  - **Viz & CLI** — Mermaid `blueprint`/`context_to_mermaid`/`trace_to_mermaid`;
278
278
  `reactifact` with `graph`/`context`/`trace`/`replay`/`branch`.
279
279
 
280
+ ## Agent Skills
281
+
282
+ reactifact ships official **Agent Skills** — reusable instructions that teach a
283
+ coding agent (Claude Code, Cursor, Codex, Copilot, …) how to build *with*
284
+ reactifact. They're bundled with the package, so they always match your version:
285
+
286
+ ```bash
287
+ python -m reactifact skills list # what is available
288
+ python -m reactifact skills install # -> .agents/skills/
289
+ python -m reactifact skills install --target both # + .claude/skills/
290
+ ```
291
+
292
+ Skills: `reactifact` (mental model), `reactifact-agents`, `reactifact-llm`,
293
+ `reactifact-rag`, `reactifact-testing`, `reactifact-eval`,
294
+ `reactifact-observability`, `reactifact-from-langchain` — see
295
+ [Agent skills](docs/en/skills.md).
296
+
280
297
  ## Run a demo
281
298
 
282
299
  Offline-capable, no API keys required (deterministic fallbacks):
@@ -334,7 +351,7 @@ uv sync --extra dev --extra web
334
351
 
335
352
  ## Status & contributing
336
353
 
337
- Active, `0.14.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
354
+ Active, `0.15.x`, pre-1.0 — the API is stabilizing and `1.0` will freeze it
338
355
  (see the [roadmap](docs/roadmap.md)); releases follow
339
356
  [Keep a Changelog](CHANGELOG.md). Questions and design discussion live in
340
357
  [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.3"
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.3"
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
@@ -47,10 +47,30 @@ DEFAULT_RELATIONS: tuple[str, ...] = (
47
47
  )
48
48
 
49
49
 
50
+ def _transient_fields(model: type[BaseModel]) -> set[str]:
51
+ """Field names a model declares as transactional *envelope*, not state.
52
+
53
+ A model lists them as ``transient_fields: ClassVar[frozenset[str]]``
54
+ (e.g. `PendingAction.dispatched_at`, `PendingQuestion.resolved_at`).
55
+ Wall-clock transitions are excluded from the content hash so an otherwise
56
+ identical state doesn't hash differently run to run — the *fact* of the
57
+ transition stays hashed (`status`/`resolution`); only its timestamp drops.
58
+ """
59
+ names = getattr(model, "transient_fields", ())
60
+ return set(names) if names else set()
61
+
62
+
50
63
  def _canonical(value: Any) -> str:
51
- """Canonical JSON for hashing (sorted keys; pydantic models dumped)."""
64
+ """Canonical JSON for hashing (sorted keys; pydantic models dumped).
65
+
66
+ A model's `transient_fields` are dropped before hashing — envelope, not
67
+ state. Everything else is serialized exactly as before, so a state with no
68
+ transient fields hashes the same as it always did.
69
+ """
52
70
  if isinstance(value, BaseModel):
53
- payload: Any = value.model_dump(mode="json")
71
+ payload: Any = value.model_dump(
72
+ mode="json", exclude=_transient_fields(type(value)) or None
73
+ )
54
74
  else:
55
75
  payload = value
56
76
  return json.dumps(payload, sort_keys=True, ensure_ascii=False, default=str)
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
- from typing import Any, Literal
4
+ from typing import Any, ClassVar, Literal
5
5
 
6
6
  from pydantic import BaseModel, Field
7
7
 
@@ -21,6 +21,11 @@ class PendingQuestion(BaseModel):
21
21
  resolution: str | None = None
22
22
  resolved_at: datetime | None = None
23
23
 
24
+ #: Wall-clock fields excluded from `context_hash` (transactional envelope,
25
+ #: not state): a resolved question's *state* is `resolution`, the moment it
26
+ #: was resolved is not something two equivalent runs must agree on.
27
+ transient_fields: ClassVar[frozenset[str]] = frozenset({"resolved_at"})
28
+
24
29
 
25
30
  class PendingAction(BaseModel):
26
31
  """An outbound side effect awaiting dispatch (outbox, §42/§55/§59).
@@ -52,3 +57,9 @@ class PendingAction(BaseModel):
52
57
  attempts: int = 0
53
58
  dispatched_at: datetime | None = None
54
59
  error: str | None = None
60
+
61
+ #: Wall-clock field excluded from `context_hash` (transactional envelope,
62
+ #: not state): the *fact* that the action dispatched (`status`/`error`/
63
+ #: `attempts`) is hashed, the moment it happened is not — so an outbox run
64
+ #: stays run-to-run reproducible.
65
+ transient_fields: ClassVar[frozenset[str]] = frozenset({"dispatched_at"})
@@ -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
  ]