reactifact 0.14.0__tar.gz → 0.14.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.14.0/reactifact.egg-info → reactifact-0.14.2}/PKG-INFO +1 -1
  2. {reactifact-0.14.0 → reactifact-0.14.2}/pyproject.toml +1 -1
  3. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/__init__.py +1 -1
  4. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/chat.py +14 -1
  5. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/chat.py +5 -0
  6. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/runtime.py +3 -3
  7. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/sessions.html +6 -6
  8. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/ui.html +14 -14
  9. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/ui_run.html +10 -10
  10. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/web.py +49 -13
  11. {reactifact-0.14.0 → reactifact-0.14.2/reactifact.egg-info}/PKG-INFO +1 -1
  12. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/SOURCES.txt +1 -0
  13. reactifact-0.14.2/tests/test_chat_outbox.py +70 -0
  14. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_tracing.py +84 -0
  15. {reactifact-0.14.0 → reactifact-0.14.2}/LICENSE +0 -0
  16. {reactifact-0.14.0 → reactifact-0.14.2}/README.md +0 -0
  17. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/__main__.py +0 -0
  18. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/_extras.py +0 -0
  19. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/_httpx.py +0 -0
  20. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/__init__.py +0 -0
  21. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/client.py +0 -0
  22. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/models.py +0 -0
  23. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/remote.py +0 -0
  24. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/a2a/server.py +0 -0
  25. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/agent_tool.py +0 -0
  26. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/agents.py +0 -0
  27. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/artifacts.py +0 -0
  28. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/audit.py +0 -0
  29. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/authz.py +0 -0
  30. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/branching.py +0 -0
  31. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/budget.py +0 -0
  32. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cache.py +0 -0
  33. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/checkpoints.py +0 -0
  34. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/__init__.py +0 -0
  35. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/branch.py +0 -0
  36. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/common.py +0 -0
  37. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/context.py +0 -0
  38. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/eval.py +0 -0
  39. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/graph.py +0 -0
  40. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/replay.py +0 -0
  41. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/scenario.py +0 -0
  42. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/skills.py +0 -0
  43. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/cli/trace.py +0 -0
  44. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/commit.py +0 -0
  45. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/commit_log.py +0 -0
  46. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/consume.py +0 -0
  47. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/context.py +0 -0
  48. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/context_builder.py +0 -0
  49. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/effects.py +0 -0
  50. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/__init__.py +0 -0
  51. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/dataset.py +0 -0
  52. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/evaluators.py +0 -0
  53. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/judge.py +0 -0
  54. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/online.py +0 -0
  55. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/runner.py +0 -0
  56. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/eval/scoring.py +0 -0
  57. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/events.py +0 -0
  58. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/guardrails.py +0 -0
  59. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/interrupt.py +0 -0
  60. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/llm_agent.py +0 -0
  61. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/logging.py +0 -0
  62. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/__init__.py +0 -0
  63. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/client.py +0 -0
  64. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/oauth.py +0 -0
  65. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/mcp/server.py +0 -0
  66. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/metrics.py +0 -0
  67. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/native_tool_use.py +0 -0
  68. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/operations.py +0 -0
  69. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/patches.py +0 -0
  70. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/pricing.py +0 -0
  71. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/produce.py +0 -0
  72. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/prompts.py +0 -0
  73. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/__init__.py +0 -0
  74. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/_retry.py +0 -0
  75. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/anthropic.py +0 -0
  76. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/azure.py +0 -0
  77. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/cerebras.py +0 -0
  78. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/chat.py +0 -0
  79. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/contracts.py +0 -0
  80. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/deepseek.py +0 -0
  81. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/fake.py +0 -0
  82. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/fireworks.py +0 -0
  83. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/gemini.py +0 -0
  84. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/github_models.py +0 -0
  85. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/groq.py +0 -0
  86. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/image.py +0 -0
  87. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/mistral.py +0 -0
  88. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/nvidia.py +0 -0
  89. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/ollama.py +0 -0
  90. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/openai.py +0 -0
  91. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/openrouter.py +0 -0
  92. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/perplexity.py +0 -0
  93. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/qwen.py +0 -0
  94. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/speech.py +0 -0
  95. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/together.py +0 -0
  96. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/video.py +0 -0
  97. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/xai.py +0 -0
  98. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/providers/zai.py +0 -0
  99. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/py.typed +0 -0
  100. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/__init__.py +0 -0
  101. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/_shared.py +0 -0
  102. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/agent.py +0 -0
  103. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/models.py +0 -0
  104. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/rag.py +0 -0
  105. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quick/tools_agent.py +0 -0
  106. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/quota.py +0 -0
  107. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/__init__.py +0 -0
  108. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/cleanup.py +0 -0
  109. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/conversation.py +0 -0
  110. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/identity.py +0 -0
  111. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/inputs.py +0 -0
  112. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/memory.py +0 -0
  113. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/plan_execute.py +0 -0
  114. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/reflection.py +0 -0
  115. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/resolve.py +0 -0
  116. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/rollback.py +0 -0
  117. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/search.py +0 -0
  118. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/skills.py +0 -0
  119. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/status.py +0 -0
  120. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/supervisor.py +0 -0
  121. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/text.py +0 -0
  122. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/recipes/tool_loop.py +0 -0
  123. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/redaction.py +0 -0
  124. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/relations.py +0 -0
  125. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/replay.py +0 -0
  126. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/request.py +0 -0
  127. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/resources.py +0 -0
  128. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/routing.py +0 -0
  129. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/scheduler.py +0 -0
  130. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/session.py +0 -0
  131. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/SKILL.md +0 -0
  132. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/references/effects-and-state.md +0 -0
  133. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact/references/trust-and-safety.md +0 -0
  134. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/SKILL.md +0 -0
  135. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/references/consume.md +0 -0
  136. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-agents/references/effects.md +0 -0
  137. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-eval/SKILL.md +0 -0
  138. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-eval/references/online.md +0 -0
  139. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-from-langchain/SKILL.md +0 -0
  140. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/SKILL.md +0 -0
  141. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/references/providers.md +0 -0
  142. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-llm/references/tools.md +0 -0
  143. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-observability/SKILL.md +0 -0
  144. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-rag/SKILL.md +0 -0
  145. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-rag/references/sources.md +0 -0
  146. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/skills/reactifact-testing/SKILL.md +0 -0
  147. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/sources.py +0 -0
  148. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/streaming.py +0 -0
  149. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/structured.py +0 -0
  150. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/__init__.py +0 -0
  151. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/assertions.py +0 -0
  152. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/exceptions.py +0 -0
  153. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/fault.py +0 -0
  154. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/golden.py +0 -0
  155. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/lab.py +0 -0
  156. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/mock.py +0 -0
  157. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/pytest_plugin.py +0 -0
  158. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/record.py +0 -0
  159. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/testing/registry.py +0 -0
  160. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tool_use.py +0 -0
  161. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tools.py +0 -0
  162. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/__init__.py +0 -0
  163. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/_otlp.py +0 -0
  164. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/columns.py +0 -0
  165. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/langfuse.py +0 -0
  166. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/models.py +0 -0
  167. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/otlp.py +0 -0
  168. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/postgres.py +0 -0
  169. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/store.py +0 -0
  170. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/templates/app.css +0 -0
  171. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/tracing/tracer.py +0 -0
  172. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/triggers.py +0 -0
  173. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/types.py +0 -0
  174. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/verify.py +0 -0
  175. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/viz.py +0 -0
  176. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact/web.py +0 -0
  177. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/dependency_links.txt +0 -0
  178. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/entry_points.txt +0 -0
  179. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/requires.txt +0 -0
  180. {reactifact-0.14.0 → reactifact-0.14.2}/reactifact.egg-info/top_level.txt +0 -0
  181. {reactifact-0.14.0 → reactifact-0.14.2}/setup.cfg +0 -0
  182. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a.py +0 -0
  183. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a_example.py +0 -0
  184. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_a2a_interop.py +0 -0
  185. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_absent_consume.py +0 -0
  186. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_adaptive.py +0 -0
  187. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_agent_tool.py +0 -0
  188. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_anthropic_provider.py +0 -0
  189. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_artifacts.py +0 -0
  190. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_audit.py +0 -0
  191. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_authz.py +0 -0
  192. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_backbone.py +0 -0
  193. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_branching.py +0 -0
  194. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_budget.py +0 -0
  195. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cache.py +0 -0
  196. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_canonical_ports.py +0 -0
  197. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_example.py +0 -0
  198. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_memory.py +0 -0
  199. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_chat_web.py +0 -0
  200. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_checkpoint.py +0 -0
  201. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_checkpoints_concurrency.py +0 -0
  202. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cli.py +0 -0
  203. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_cli_scenario.py +0 -0
  204. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_commit_log.py +0 -0
  205. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_compaction.py +0 -0
  206. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_concurrency.py +0 -0
  207. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_consumes_produces.py +0 -0
  208. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_context_builder.py +0 -0
  209. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_correlated_consume.py +0 -0
  210. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_debounce.py +0 -0
  211. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_determinism.py +0 -0
  212. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops.py +0 -0
  213. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops_online.py +0 -0
  214. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_devops_web.py +0 -0
  215. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_effects.py +0 -0
  216. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval.py +0 -0
  217. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_dataset.py +0 -0
  218. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_judge.py +0 -0
  219. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_online.py +0 -0
  220. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_eval_trajectory.py +0 -0
  221. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_fintech_audit.py +0 -0
  222. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_forklab.py +0 -0
  223. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_forklab_web.py +0 -0
  224. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_friendly_api.py +0 -0
  225. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_gemini_provider.py +0 -0
  226. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_golden.py +0 -0
  227. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_guardrails.py +0 -0
  228. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_hitl.py +0 -0
  229. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_image_provider.py +0 -0
  230. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_incident_commander.py +0 -0
  231. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_invalidation.py +0 -0
  232. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_join_consume.py +0 -0
  233. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_knowledge.py +0 -0
  234. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_knowledge_web.py +0 -0
  235. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_llm_ladder.py +0 -0
  236. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_logging.py +0 -0
  237. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_loop_bound_client.py +0 -0
  238. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_mcp.py +0 -0
  239. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_medic_lab.py +0 -0
  240. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_medic_lab_web.py +0 -0
  241. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_metrics.py +0 -0
  242. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_multisource.py +0 -0
  243. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_native_tool_use.py +0 -0
  244. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_openai_provider.py +0 -0
  245. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_outbox.py +0 -0
  246. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_patches.py +0 -0
  247. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_pending_actions.py +0 -0
  248. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_ports.py +0 -0
  249. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_produce_styles.py +0 -0
  250. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_produce_trigger.py +0 -0
  251. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_prompts.py +0 -0
  252. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_provider_auth.py +0 -0
  253. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_provider_retry.py +0 -0
  254. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_providers_integration.py +0 -0
  255. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_public_api.py +0 -0
  256. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_quick.py +0 -0
  257. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_quota.py +0 -0
  258. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_reacts_to.py +0 -0
  259. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_conversation.py +0 -0
  260. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_plan_execute.py +0 -0
  261. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_reflection.py +0 -0
  262. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_supervisor.py +0 -0
  263. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipe_tool_loop.py +0 -0
  264. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes.py +0 -0
  265. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_inputs.py +0 -0
  266. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_memory.py +0 -0
  267. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_recipes_skills.py +0 -0
  268. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_redaction.py +0 -0
  269. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_relation_graph.py +0 -0
  270. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_relations.py +0 -0
  271. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repair.py +0 -0
  272. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repair_web.py +0 -0
  273. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_replay.py +0 -0
  274. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_repo_agent.py +0 -0
  275. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_request.py +0 -0
  276. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_research.py +0 -0
  277. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_resources.py +0 -0
  278. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_retry.py +0 -0
  279. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_routing.py +0 -0
  280. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_runtime.py +0 -0
  281. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_runtime_errors.py +0 -0
  282. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_scheduler_semantics.py +0 -0
  283. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sessions.py +0 -0
  284. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_shutdown.py +0 -0
  285. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_skills.py +0 -0
  286. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources.py +0 -0
  287. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources_search.py +0 -0
  288. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_sources_vector.py +0 -0
  289. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_speech_provider.py +0 -0
  290. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_starter_app.py +0 -0
  291. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_streaming.py +0 -0
  292. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_structured.py +0 -0
  293. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_support_copilot.py +0 -0
  294. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_assertions.py +0 -0
  295. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_golden.py +0 -0
  296. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_lab.py +0 -0
  297. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_mock.py +0 -0
  298. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_provenance.py +0 -0
  299. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_pytest_plugin.py +0 -0
  300. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_testing_registry.py +0 -0
  301. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_token_budget.py +0 -0
  302. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_tools.py +0 -0
  303. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_trace_columns.py +0 -0
  304. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_trace_tags.py +0 -0
  305. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_type_registry.py +0 -0
  306. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_vendor_factories.py +0 -0
  307. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_vendor_multimodal_factories.py +0 -0
  308. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_verify.py +0 -0
  309. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_video_provider.py +0 -0
  310. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_view.py +0 -0
  311. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_viz.py +0 -0
  312. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_web_source.py +0 -0
  313. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_workspace.py +0 -0
  314. {reactifact-0.14.0 → reactifact-0.14.2}/tests/test_workspace_with_sources.py +0 -0
  315. {reactifact-0.14.0 → reactifact-0.14.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.14.0
3
+ Version: 0.14.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "reactifact"
7
- version = "0.14.0"
7
+ version = "0.14.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.14.0"
54
+ __version__ = "0.14.2"
55
55
 
56
56
  __all__ = [
57
57
  "Agent",
@@ -38,7 +38,7 @@ from .consume import Consume
38
38
  from .context import Context
39
39
  from .events import Event
40
40
  from .resources import RuntimeResources
41
- from .runtime import Runtime
41
+ from .runtime import Dispatcher, DispatchErrorHandler, Runtime
42
42
  from .session import Session, SessionStore
43
43
 
44
44
  logger = logging.getLogger("reactifact.chat")
@@ -306,6 +306,13 @@ class ChatAssistant:
306
306
  multi-stage pipeline routinely produces several generations per turn (see
307
307
  `Runtime.__init__`'s own docstring for the trade-off).
308
308
 
309
+ `dispatcher=` (and `on_dispatch_error=`) also passes straight through to
310
+ `Runtime`: give an agent that uses `effects.act(...)` a way to actually send
311
+ its outbound side effects from a chat turn, with the same once-per-stable-id
312
+ delivery `Runtime(dispatcher=...)` provides. Without it, a chat turn that
313
+ records a `PendingAction` leaves it `pending` (the runtime warns) for someone
314
+ to drain later.
315
+
309
316
  `resources=`/`create_message=` may optionally take the current turn's
310
317
  `session_id` — `resources=lambda session_id: build_resources(session_id)`
311
318
  (e.g. to attach an authenticated user looked up from the session) and
@@ -349,6 +356,8 @@ class ChatAssistant:
349
356
  on_agent_error: Callable[[Agent, Event, BaseException], None] | None = None,
350
357
  session_save_policy: Literal["per_commit", "per_turn"] = "per_commit",
351
358
  memory: ChatMemory | None = None,
359
+ dispatcher: Dispatcher | None = None,
360
+ on_dispatch_error: DispatchErrorHandler | None = None,
352
361
  ):
353
362
  self.store = store
354
363
  self._agents = agents
@@ -368,6 +377,8 @@ class ChatAssistant:
368
377
  session_save_policy
369
378
  )
370
379
  self._memory = memory
380
+ self._dispatcher = dispatcher
381
+ self._on_dispatch_error = on_dispatch_error
371
382
  # Serializes concurrent turns on the *same* session_id (a double
372
383
  # submit, a client retry): without this, two overlapping stream()
373
384
  # calls both load the same starting state and the later save() wins,
@@ -453,6 +464,8 @@ class ChatAssistant:
453
464
  isolate_errors=self._isolate_errors,
454
465
  on_agent_error=self._on_agent_error,
455
466
  session_save_policy=self._session_save_policy,
467
+ dispatcher=self._dispatcher,
468
+ on_dispatch_error=self._on_dispatch_error,
456
469
  )
457
470
 
458
471
  async def stream(
@@ -15,6 +15,7 @@ from ..checkpoints import FileKVBackend, InMemoryKVBackend
15
15
  from ..context import Context
16
16
  from ..providers import LLMProvider, from_env
17
17
  from ..resources import RuntimeResources
18
+ from ..runtime import Dispatcher, DispatchErrorHandler
18
19
  from ..session import SessionStore
19
20
  from .models import Question
20
21
 
@@ -54,6 +55,8 @@ def chat_agent(
54
55
  resources: RuntimeResources | Callable[..., RuntimeResources] | None = None,
55
56
  budget: Budget | None = None,
56
57
  tracer: Any = None,
58
+ dispatcher: Dispatcher | None = None,
59
+ on_dispatch_error: DispatchErrorHandler | None = None,
57
60
  ) -> ChatAssistant:
58
61
  """A configured `ChatAssistant` — sessions, turns, history, out of the box.
59
62
 
@@ -79,6 +82,8 @@ def chat_agent(
79
82
  resources=resources,
80
83
  budget=budget,
81
84
  tracer=tracer,
85
+ dispatcher=dispatcher,
86
+ on_dispatch_error=on_dispatch_error,
82
87
  )
83
88
 
84
89
 
@@ -38,6 +38,8 @@ AgentResult = tuple[Patch | None, Agent, Event, list[Read], float, BaseException
38
38
  #: the intent is committed; the app performs the real I/O and returns (or
39
39
  #: raises). Must be idempotent on `idempotency_key` (§42).
40
40
  Dispatcher = Callable[[Context, Artifact[PendingAction]], Awaitable[None]]
41
+ #: Called when the dispatcher raises (the action is already marked `failed`).
42
+ DispatchErrorHandler = Callable[[Artifact[PendingAction], BaseException], None]
41
43
 
42
44
 
43
45
  @dataclass
@@ -93,9 +95,7 @@ class Runtime:
93
95
  on_agent_error: Callable[[Agent, Event, BaseException], None] | None = None,
94
96
  session_save_policy: Literal["per_commit", "per_turn"] = "per_commit",
95
97
  dispatcher: Dispatcher | None = None,
96
- on_dispatch_error: (
97
- Callable[[Artifact[PendingAction], BaseException], None] | None
98
- ) = None,
98
+ on_dispatch_error: DispatchErrorHandler | None = None,
99
99
  ):
100
100
  self.context = context
101
101
  self.agents = agents or []
@@ -4,22 +4,22 @@
4
4
  <meta charset="utf-8"/>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
6
  <title>reactifact sessions</title>
7
- <link rel="stylesheet" href="/traces/assets/app.css"/>
7
+ <link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
8
8
  <script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
9
9
  </head>
10
10
  <body>
11
11
  <div class="app" id="app">
12
12
  <aside class="sidebar">
13
- <a class="brand" href="/traces">
13
+ <a class="brand" href="__BASE__/traces">
14
14
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
15
15
  <span class="name">reactifact <em>traces</em></span>
16
16
  </a>
17
17
  <nav class="nav">
18
- <a class="nav-item" href="/traces">
18
+ <a class="nav-item" href="__BASE__/traces">
19
19
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
20
20
  <span class="nav-label">Traces</span>
21
21
  </a>
22
- <a class="nav-item active" href="/sessions">
22
+ <a class="nav-item active" href="__BASE__/sessions">
23
23
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
24
24
  <span class="nav-label">Sessions</span>
25
25
  </a>
@@ -97,7 +97,7 @@ async function load(){
97
97
  const p=new URLSearchParams({limit:state.limit,offset:state.offset});
98
98
  if(state.q)p.set('q',state.q);
99
99
  try{
100
- const res=await fetch('/api/sessions?'+p);
100
+ const res=await fetch('__BASE__/api/sessions?'+p);
101
101
  if(!res.ok)throw new Error('HTTP '+res.status);
102
102
  const data=await res.json();
103
103
  state.items=data.items||[];state.total=data.total||0;render();
@@ -133,7 +133,7 @@ function updatePager(){
133
133
  }
134
134
  document.getElementById('rows').addEventListener('click',(e)=>{
135
135
  const row=e.target.closest('tr.row');if(!row)return;
136
- window.location.href='/traces?session_id='+encodeURIComponent(row.dataset.id);
136
+ window.location.href='__BASE__/traces?session_id='+encodeURIComponent(row.dataset.id);
137
137
  });
138
138
  document.getElementById('f-q').addEventListener('input',debounce((e)=>{state.q=e.target.value.trim();state.offset=0;load();}));
139
139
  document.getElementById('prev').addEventListener('click',()=>{state.offset=Math.max(0,state.offset-state.limit);load();});
@@ -4,22 +4,22 @@
4
4
  <meta charset="utf-8"/>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
6
  <title>reactifact traces</title>
7
- <link rel="stylesheet" href="/traces/assets/app.css"/>
7
+ <link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
8
8
  <script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
9
9
  </head>
10
10
  <body>
11
11
  <div class="app" id="app">
12
12
  <aside class="sidebar">
13
- <a class="brand" href="/traces">
13
+ <a class="brand" href="__BASE__/traces">
14
14
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
15
15
  <span class="name">reactifact <em>traces</em></span>
16
16
  </a>
17
17
  <nav class="nav">
18
- <a class="nav-item active" href="/traces">
18
+ <a class="nav-item active" href="__BASE__/traces">
19
19
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
20
20
  <span class="nav-label">Traces</span>
21
21
  </a>
22
- <a class="nav-item" href="/sessions">
22
+ <a class="nav-item" href="__BASE__/sessions">
23
23
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
24
24
  <span class="nav-label">Sessions</span>
25
25
  </a>
@@ -188,7 +188,7 @@ function loadColumnPref(){try{const raw=localStorage.getItem(COLS_KEY);VISIBLE_C
188
188
  function saveColumnPref(){try{localStorage.setItem(COLS_KEY,JSON.stringify(VISIBLE_COLS||[]));}catch(_){}}
189
189
  function visibleColumns(){const all=state.columns.map(c=>c.label);if(VISIBLE_COLS===null)return all;return all.filter(l=>VISIBLE_COLS.includes(l));}
190
190
  async function loadColumns(){
191
- try{const r=await fetch('/api/columns');state.columns=(await r.json()).items||[];}
191
+ try{const r=await fetch('__BASE__/api/columns');state.columns=(await r.json()).items||[];}
192
192
  catch(_){state.columns=[];}
193
193
  render();
194
194
  }
@@ -287,7 +287,7 @@ function filters(){
287
287
  }
288
288
 
289
289
  async function loadFacets(){
290
- try{const r=await fetch('/api/tags');const d=await r.json();state.facets=d.items||[];}
290
+ try{const r=await fetch('__BASE__/api/tags');const d=await r.json();state.facets=d.items||[];}
291
291
  catch(_){state.facets=[];}
292
292
  renderFacets();
293
293
  }
@@ -308,7 +308,7 @@ function renderFacets(){
308
308
  async function load(){
309
309
  const q=filters();q.set('limit',state.limit);q.set('offset',state.offset);
310
310
  try{
311
- const res=await fetch('/api/traces?'+q);
311
+ const res=await fetch('__BASE__/api/traces?'+q);
312
312
  if(!res.ok)throw new Error('HTTP '+res.status);
313
313
  const data=await res.json();
314
314
  state.runs=data.items||[];state.total=data.total||0;state.stats=data.stats||null;
@@ -454,7 +454,7 @@ document.getElementById('view-del').addEventListener('click',()=>{
454
454
  const sel=document.getElementById('views');if(sel.value==='')return;
455
455
  const views=getViews();views.splice(Number(sel.value),1);setViews(views);renderViews();toast('View deleted');
456
456
  });
457
- document.getElementById('export').addEventListener('click',()=>{const q=filters();q.set('limit',500);window.location.href='/api/traces/export?'+q;});
457
+ document.getElementById('export').addEventListener('click',()=>{const q=filters();q.set('limit',500);window.location.href='__BASE__/api/traces/export?'+q;});
458
458
  document.getElementById('live').addEventListener('click',(e)=>{
459
459
  state.live=!state.live;e.currentTarget.classList.toggle('on',state.live);
460
460
  if(state.live){timer=setInterval(()=>{if(!document.hidden)load();},3000);}else{clearInterval(timer);}
@@ -462,7 +462,7 @@ document.getElementById('live').addEventListener('click',(e)=>{
462
462
  document.getElementById('runs').addEventListener('click',(e)=>{
463
463
  if(e.target.closest('.rowcheck'))return;
464
464
  const row=e.target.closest('tr.row');if(!row)return;
465
- window.location.href='/traces/'+row.dataset.id;
465
+ window.location.href='__BASE__/traces/'+row.dataset.id;
466
466
  });
467
467
  document.getElementById('runs').addEventListener('change',(e)=>{
468
468
  const c=e.target.closest('.rowcheck');if(!c)return;
@@ -476,13 +476,13 @@ document.getElementById('bulk-clear').addEventListener('click',()=>{state.select
476
476
  document.getElementById('bulk-apply').addEventListener('click',async()=>{
477
477
  const tag=document.getElementById('bulk-tag').value.trim();if(!tag)return;
478
478
  const note=document.getElementById('bulk-note').value;
479
- await fetch('/api/traces/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag,note:note})});
479
+ await fetch('__BASE__/api/traces/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag,note:note})});
480
480
  document.getElementById('bulk-tag').value='';document.getElementById('bulk-note').value='';
481
481
  state.selected.clear();toast('Tagged');await refresh();
482
482
  });
483
483
  document.getElementById('bulk-remove').addEventListener('click',async()=>{
484
484
  const tag=document.getElementById('bulk-remove-sel').value;if(!tag)return;
485
- await fetch('/api/traces/tags/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag})});
485
+ await fetch('__BASE__/api/traces/tags/remove',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({run_ids:Array.from(state.selected),tag:tag})});
486
486
  state.selected.clear();toast('Tag removed');await refresh();
487
487
  });
488
488
 
@@ -507,18 +507,18 @@ document.getElementById('tagrows').addEventListener('click',async(e)=>{
507
507
  if(e.target.closest('.row-save')){
508
508
  const name=row.querySelector('.row-name').value.trim();
509
509
  const color=row.querySelector('.row-color').value;
510
- await fetch('/api/tags/'+encodeURIComponent(old),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({new_name:name,color:color})});
510
+ await fetch('__BASE__/api/tags/'+encodeURIComponent(old),{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({new_name:name,color:color})});
511
511
  await refresh();renderTagRows();toast('Tag updated');
512
512
  }else if(e.target.closest('.row-del')){
513
513
  if(!confirm('Delete tag "'+old+'" from every run?'))return;
514
- await fetch('/api/tags/'+encodeURIComponent(old),{method:'DELETE'});
514
+ await fetch('__BASE__/api/tags/'+encodeURIComponent(old),{method:'DELETE'});
515
515
  await refresh();renderTagRows();toast('Tag deleted');
516
516
  }
517
517
  });
518
518
  document.getElementById('new-tag-add').addEventListener('click',async()=>{
519
519
  const name=document.getElementById('new-tag-name').value.trim();if(!name)return;
520
520
  const color=document.getElementById('new-tag-color').value;
521
- await fetch('/api/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,color:color})});
521
+ await fetch('__BASE__/api/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,color:color})});
522
522
  document.getElementById('new-tag-name').value='';await refresh();renderTagRows();toast('Tag added');
523
523
  });
524
524
 
@@ -4,22 +4,22 @@
4
4
  <meta charset="utf-8"/>
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
6
6
  <title>reactifact trace</title>
7
- <link rel="stylesheet" href="/traces/assets/app.css"/>
7
+ <link rel="stylesheet" href="__BASE__/traces/assets/app.css"/>
8
8
  <script>(function(){try{var s=localStorage.getItem('reactifact-theme');var d=s?s==='dark':window.matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.classList.toggle('dark',d);}catch(e){document.documentElement.classList.add('dark');}})();</script>
9
9
  </head>
10
10
  <body>
11
11
  <div class="app" id="app">
12
12
  <aside class="sidebar">
13
- <a class="brand" href="/traces">
13
+ <a class="brand" href="__BASE__/traces">
14
14
  <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5" cy="12" r="2.6" fill="currentColor"/><circle cx="19" cy="5" r="2.6" fill="currentColor"/><circle cx="19" cy="19" r="2.6" fill="currentColor"/><path d="M7.4 11 16.6 6M7.4 13l9.2 5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
15
15
  <span class="name">reactifact <em>traces</em></span>
16
16
  </a>
17
17
  <nav class="nav">
18
- <a class="nav-item active" href="/traces">
18
+ <a class="nav-item active" href="__BASE__/traces">
19
19
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="5" cy="12" r="2.2"/><circle cx="19" cy="5" r="2.2"/><circle cx="19" cy="19" r="2.2"/><path d="M7.2 11 16.8 6M7.2 13l9.6 5"/></svg>
20
20
  <span class="nav-label">Traces</span>
21
21
  </a>
22
- <a class="nav-item" href="/sessions">
22
+ <a class="nav-item" href="__BASE__/sessions">
23
23
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v11H8l-4 4z"/></svg>
24
24
  <span class="nav-label">Sessions</span>
25
25
  </a>
@@ -42,7 +42,7 @@
42
42
  <button class="icon-btn" id="menu-btn" aria-label="open navigation">
43
43
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
44
44
  </button>
45
- <div class="crumb"><a href="/traces">traces</a><span class="sep">/</span><b id="cid">#…</b><span id="head"></span></div>
45
+ <div class="crumb"><a href="__BASE__/traces">traces</a><span class="sep">/</span><b id="cid">#…</b><span id="head"></span></div>
46
46
  <div class="spacer"></div>
47
47
  <button class="btn sm" id="copy-link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.1 0l3-3a5 5 0 0 0-7.1-7.1L11 4.9"/><path d="M14 11a5 5 0 0 0-7.1 0l-3 3A5 5 0 0 0 11 21.1l1.9-1.9"/></svg><span data-label>link</span></button>
48
48
  <button class="btn sm" id="copy-json"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg><span data-label>json</span></button>
@@ -104,7 +104,7 @@ function toast(msg){let t=document.getElementById('toast');if(!t){t=document.cre
104
104
 
105
105
  async function load(){
106
106
  try{
107
- const res=await fetch('/api/traces/'+RUN_ID);
107
+ const res=await fetch('__BASE__/api/traces/'+RUN_ID);
108
108
  if(res.status===404){document.getElementById('cid').textContent='#'+RUN_ID+' — not found';return;}
109
109
  if(!res.ok)throw new Error('HTTP '+res.status);
110
110
  render(await res.json());
@@ -122,7 +122,7 @@ function hex(c){return /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(
122
122
  let VOCAB=[];
123
123
  let CURRENT=[];
124
124
  async function loadVocab(){
125
- try{const r=await fetch('/api/tags');const d=await r.json();VOCAB=(d.items||[]).map(x=>x.name);}
125
+ try{const r=await fetch('__BASE__/api/tags');const d=await r.json();VOCAB=(d.items||[]).map(x=>x.name);}
126
126
  catch(_){VOCAB=[];}
127
127
  const dl=document.getElementById('taglist');
128
128
  if(dl)dl.innerHTML=VOCAB.map(n=>'<option value="'+escapeHtml(n)+'">').join('');
@@ -152,18 +152,18 @@ function renderAnnot(list){
152
152
  document.getElementById('annot-note').addEventListener('keydown',(e)=>{if(e.key==='Enter'){e.preventDefault();addTag();}});
153
153
  }
154
154
  async function reloadTrace(){
155
- const res=await fetch('/api/traces/'+RUN_ID);
155
+ const res=await fetch('__BASE__/api/traces/'+RUN_ID);
156
156
  if(res.ok)renderAnnot((await res.json()).annotations||[]);
157
157
  await loadVocab();
158
158
  }
159
159
  async function addTag(){
160
160
  const tag=document.getElementById('annot-tag').value.trim();if(!tag)return;
161
161
  const note=document.getElementById('annot-note').value;
162
- await fetch('/api/traces/'+RUN_ID+'/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tag:tag,note:note})});
162
+ await fetch('__BASE__/api/traces/'+RUN_ID+'/tags',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({tag:tag,note:note})});
163
163
  toast('Tag added');await reloadTrace();
164
164
  }
165
165
  async function removeTag(tag){
166
- await fetch('/api/traces/'+RUN_ID+'/tags/'+encodeURIComponent(tag),{method:'DELETE'});
166
+ await fetch('__BASE__/api/traces/'+RUN_ID+'/tags/'+encodeURIComponent(tag),{method:'DELETE'});
167
167
  toast('Tag removed');await reloadTrace();
168
168
  }
169
169
 
@@ -33,11 +33,41 @@ from .columns import TraceColumn
33
33
  from .store import TraceStoreProtocol
34
34
 
35
35
  if TYPE_CHECKING:
36
- from fastapi import APIRouter
36
+ from fastapi import APIRouter, Request
37
37
 
38
38
  _TEMPLATES = Path(__file__).parent / "templates"
39
39
 
40
40
 
41
+ def _mount_base(request: Request, logical_path: str) -> str:
42
+ """Path prefix this router is mounted under — ``include_router(prefix=…)``
43
+ and/or the ASGI ``root_path`` — so templates can emit prefix-aware
44
+ absolute URLs (``/*`` links and ``fetch`` paths) instead of assuming the
45
+ service sits at the origin root.
46
+
47
+ The prefix is everything before the *last* occurrence of this route's first
48
+ logical segment (``/traces`` / ``/sessions``) in the request path. The last
49
+ occurrence — not the first — because the mount prefix may itself contain
50
+ that segment (``prefix="/api/traces"`` → ``/api/traces/traces``), while the
51
+ route's own segment is always the one nearest the path parameters.
52
+ """
53
+ root_path = request.scope.get("root_path", "") or ""
54
+ marker = "/" + logical_path.strip("/").split("/")[0]
55
+ index = request.url.path.rfind(marker)
56
+ return root_path + (request.url.path[:index] if index > 0 else "")
57
+
58
+
59
+ def _render_page(
60
+ request: Request, filename: str, logical_path: str, **replacements: str
61
+ ) -> str:
62
+ """Read a UI template and fill ``__BASE__`` (mount prefix) plus any
63
+ caller placeholders (e.g. ``__RUN_ID__``)."""
64
+ text = (_TEMPLATES / filename).read_text(encoding="utf-8")
65
+ text = text.replace("__BASE__", _mount_base(request, logical_path))
66
+ for key, value in replacements.items():
67
+ text = text.replace(key, value)
68
+ return text
69
+
70
+
41
71
  class TagAssign(BaseModel):
42
72
  tag: str = Field(min_length=1, max_length=64)
43
73
  note: str = ""
@@ -93,8 +123,13 @@ def create_trace_router(
93
123
 
94
124
  require_extra("tracing.web.create_trace_router", "fastapi", "web")
95
125
 
96
- from fastapi import APIRouter, Depends, HTTPException, Query
126
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request
97
127
  from fastapi.responses import HTMLResponse, JSONResponse, Response
128
+
129
+ # `from __future__ import annotations` keeps every hint a string, resolved
130
+ # against this module's globals at decoration time; fastapi is imported
131
+ # lazily here, so publish the names the page handlers annotate with.
132
+ globals()["Request"] = Request
98
133
  from fastapi.security import HTTPBasic, HTTPBasicCredentials
99
134
 
100
135
  dependencies = []
@@ -286,26 +321,27 @@ def create_trace_router(
286
321
  )
287
322
 
288
323
  @router.get("/traces", response_class=HTMLResponse)
289
- async def traces_list_page() -> str:
290
- return (_TEMPLATES / "ui.html").read_text(encoding="utf-8")
324
+ async def traces_list_page(request: Request) -> str:
325
+ return _render_page(request, "ui.html", "/traces")
291
326
 
292
327
  @router.get("/sessions", response_class=HTMLResponse)
293
- async def sessions_list_page() -> str:
294
- return (_TEMPLATES / "sessions.html").read_text(encoding="utf-8")
328
+ async def sessions_list_page(request: Request) -> str:
329
+ return _render_page(request, "sessions.html", "/sessions")
295
330
 
296
331
  @router.get("/traces/{trace_id}", response_class=HTMLResponse)
297
- async def traces_run_page(trace_id: str) -> str:
332
+ async def traces_run_page(request: Request, trace_id: str) -> str:
298
333
  from ..viz import trace_provenance_to_mermaid, trace_to_mermaid
299
334
 
300
335
  trace = await store.get(trace_id)
301
336
  mermaid = trace_to_mermaid(trace) if trace is not None else ""
302
337
  provenance = trace_provenance_to_mermaid(trace) if trace is not None else ""
303
- return (
304
- (_TEMPLATES / "ui_run.html")
305
- .read_text(encoding="utf-8")
306
- .replace("__RUN_ID__", json.dumps(trace_id))
307
- .replace("__MERMAID__", json.dumps(mermaid))
308
- .replace("__MERMAID_GRAPH__", json.dumps(provenance))
338
+ return _render_page(
339
+ request,
340
+ "ui_run.html",
341
+ "/traces/{trace_id}",
342
+ __RUN_ID__=json.dumps(trace_id),
343
+ __MERMAID__=json.dumps(mermaid),
344
+ __MERMAID_GRAPH__=json.dumps(provenance),
309
345
  )
310
346
 
311
347
  return router
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reactifact
3
- Version: 0.14.0
3
+ Version: 0.14.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
@@ -192,6 +192,7 @@ tests/test_cache.py
192
192
  tests/test_canonical_ports.py
193
193
  tests/test_chat_example.py
194
194
  tests/test_chat_memory.py
195
+ tests/test_chat_outbox.py
195
196
  tests/test_chat_web.py
196
197
  tests/test_checkpoint.py
197
198
  tests/test_checkpoints_concurrency.py
@@ -0,0 +1,70 @@
1
+ """`ChatAssistant(dispatcher=...)` — outbound side effects from a chat turn."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ from pydantic import BaseModel
8
+ from reactifact import PendingAction
9
+ from reactifact.agents import create_agent
10
+ from reactifact.chat import ChatAssistant
11
+ from reactifact.checkpoints import InMemoryKVBackend
12
+ from reactifact.consume import Consume
13
+ from reactifact.context import Context
14
+ from reactifact.produce import Produce, ProduceCall
15
+ from reactifact.session import SessionStore
16
+
17
+
18
+ class _Msg(BaseModel):
19
+ text: str
20
+
21
+
22
+ class _Reply(BaseModel):
23
+ text: str
24
+
25
+
26
+ class _Notifier(Produce[_Reply]):
27
+ artifact_type = _Reply
28
+ also_creates = (PendingAction,)
29
+
30
+ async def produce(self, call: ProduceCall) -> None:
31
+ msg = call.trigger
32
+ if msg is None:
33
+ return
34
+ call.effects.act(
35
+ "notify", key=f"notify:{msg.data.text}", payload={"text": msg.data.text}
36
+ )
37
+ call.effects.create(_Reply(text=f"noted: {msg.data.text}"), id="reply:1")
38
+
39
+
40
+ def _reply(ctx: Context, _message_id: str) -> dict[str, str]:
41
+ replies = ctx.list_artifacts(_Reply)
42
+ return {"reply": replies[-1].data.text if replies else ""}
43
+
44
+
45
+ def test_chat_assistant_runs_the_outbox_dispatcher():
46
+ sent: list[str] = []
47
+
48
+ async def dispatcher(context: Context, action) -> None:
49
+ sent.append(action.data.idempotency_key)
50
+
51
+ store = SessionStore(InMemoryKVBackend())
52
+ assistant = ChatAssistant(
53
+ store=store,
54
+ agents=[
55
+ create_agent("notify", consumes=[Consume(_Msg)], produces=[_Notifier()])
56
+ ],
57
+ user_message=_Msg,
58
+ reply=_reply,
59
+ dispatcher=dispatcher,
60
+ )
61
+
62
+ result = asyncio.run(assistant.invoke("hi", session_id="s1"))
63
+
64
+ assert result["reply"] == "noted: hi"
65
+ assert sent == ["notify:hi"]
66
+
67
+ session = asyncio.run(store.open("s1"))
68
+ action = session.context.get("action:notify:hi")
69
+ assert action is not None
70
+ assert action.data.status == "dispatched"
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import json
3
+ import re
3
4
 
4
5
  from pydantic import BaseModel
5
6
  from reactifact import Agent, Consume, Context, Patch, Runtime, RuntimeResources
@@ -868,3 +869,86 @@ def test_recording_llm_prompt_hash_defaults_empty():
868
869
  llm = RecordingLLM(FakeLLM("x"), on_call=recorded.append, agent_of=lambda: "a")
869
870
  asyncio.run(llm.complete(LLMRequest(messages=[Message.user("hi")])))
870
871
  assert recorded[0].prompt_hash == ""
872
+
873
+
874
+ def test_trace_pages_are_prefix_aware(tmp_path):
875
+ from fastapi import FastAPI
876
+ from fastapi.testclient import TestClient
877
+ from reactifact.tracing.web import create_trace_router
878
+
879
+ store = TraceStore(str(tmp_path / "prefix.db"))
880
+ run(store.export(RunTrace(id="r1", session_id="s", outcome="completed")))
881
+ app = FastAPI()
882
+ app.include_router(create_trace_router(store), prefix="/ai")
883
+ client = TestClient(app)
884
+
885
+ listing = client.get("/ai/traces")
886
+ assert listing.status_code == 200
887
+ html = listing.text
888
+ assert "__BASE__" not in html
889
+ assert 'href="/ai/traces/assets/app.css"' in html
890
+ assert 'href="/ai/traces"' in html
891
+ assert 'href="/ai/sessions"' in html
892
+ assert "'/ai/api/traces" in html
893
+ assert "'/ai/api/columns" in html
894
+ assert "'/ai/traces/'" in html
895
+
896
+ sessions = client.get("/ai/sessions").text
897
+ assert "__BASE__" not in sessions
898
+ assert "'/ai/api/sessions" in sessions
899
+ assert 'href="/ai/traces"' in sessions
900
+
901
+ run_page = client.get("/ai/traces/r1").text
902
+ assert "__BASE__" not in run_page
903
+ assert "'/ai/api/traces/" in run_page
904
+ assert 'href="/ai/traces/assets/app.css"' in run_page
905
+
906
+
907
+ def test_trace_pages_without_prefix_keep_root_paths(tmp_path):
908
+ from fastapi import FastAPI
909
+ from fastapi.testclient import TestClient
910
+ from reactifact.tracing.web import create_trace_router
911
+
912
+ store = TraceStore(str(tmp_path / "root.db"))
913
+ app = FastAPI()
914
+ app.include_router(create_trace_router(store))
915
+ client = TestClient(app)
916
+ html = client.get("/traces").text
917
+ assert "__BASE__" not in html
918
+ assert 'href="/traces/assets/app.css"' in html
919
+ assert "'/api/traces" in html
920
+
921
+
922
+ def test_trace_pages_honor_asgi_root_path(tmp_path):
923
+ from fastapi import FastAPI
924
+ from fastapi.testclient import TestClient
925
+ from reactifact.tracing.web import create_trace_router
926
+
927
+ store = TraceStore(str(tmp_path / "rootpath.db"))
928
+ app = FastAPI(root_path="/ai")
929
+ app.include_router(create_trace_router(store))
930
+ client = TestClient(app)
931
+ html = client.get("/traces").text
932
+ assert "__BASE__" not in html
933
+ assert 'href="/ai/traces/assets/app.css"' in html
934
+ assert "'/ai/api/traces" in html
935
+
936
+
937
+ def test_trace_pages_stay_prefixed_when_prefix_contains_a_route_segment(tmp_path):
938
+ """A mount prefix that itself contains ``/traces`` (or ``/sessions``) must
939
+ not confuse the base: every absolute ref stays inside the prefix."""
940
+ from fastapi import FastAPI
941
+ from fastapi.testclient import TestClient
942
+ from reactifact.tracing.web import create_trace_router
943
+
944
+ store = TraceStore(str(tmp_path / "tricky.db"))
945
+ run(store.export(RunTrace(id="r1", session_id="s", outcome="completed")))
946
+ for prefix in ("/api/traces", "/traces-app", "/sessions", "/traces"):
947
+ app = FastAPI()
948
+ app.include_router(create_trace_router(store), prefix=prefix)
949
+ client = TestClient(app)
950
+ for logical in ("/traces", "/sessions", "/traces/r1"):
951
+ html = client.get(prefix + logical).text
952
+ assert "__BASE__" not in html
953
+ refs = re.findall(r'(?:href="|fetch\(\')(/[^"\'?]*)', html)
954
+ assert all(u.startswith(prefix) for u in refs), (prefix, logical, refs[:5])
File without changes
File without changes