flowra 0.0.91__tar.gz → 0.0.92.dev109__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 (357) hide show
  1. {flowra-0.0.91 → flowra-0.0.92.dev109}/CHANGELOG.md +28 -0
  2. {flowra-0.0.91 → flowra-0.0.92.dev109}/PKG-INFO +1 -1
  3. {flowra-0.0.91 → flowra-0.0.92.dev109}/context7.json +3 -3
  4. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/agents.md +5 -2
  5. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/observability.md +33 -2
  6. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/tools.md +3 -2
  7. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/__init__.py +2 -0
  8. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/agent.py +53 -5
  9. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/hook_events.py +24 -2
  10. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/tool_call/agent.py +11 -0
  11. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/version.py +1 -1
  12. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_tool_call_agent.py +69 -2
  13. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_turn_agent.py +381 -2
  14. {flowra-0.0.91 → flowra-0.0.92.dev109}/.agents/skills/source-command-update-pricing/SKILL.md +0 -0
  15. {flowra-0.0.91 → flowra-0.0.92.dev109}/.claude/commands/update-pricing.md +0 -0
  16. {flowra-0.0.91 → flowra-0.0.92.dev109}/.env.example +0 -0
  17. {flowra-0.0.91 → flowra-0.0.92.dev109}/.github/CODEOWNERS +0 -0
  18. {flowra-0.0.91 → flowra-0.0.92.dev109}/.github/workflows/master.yml +0 -0
  19. {flowra-0.0.91 → flowra-0.0.92.dev109}/.github/workflows/publish.yml +0 -0
  20. {flowra-0.0.91 → flowra-0.0.92.dev109}/.github/workflows/pull_request.yml +0 -0
  21. {flowra-0.0.91 → flowra-0.0.92.dev109}/.gitignore +0 -0
  22. {flowra-0.0.91 → flowra-0.0.92.dev109}/.python-version +0 -0
  23. {flowra-0.0.91 → flowra-0.0.92.dev109}/AGENTS.md +0 -0
  24. {flowra-0.0.91 → flowra-0.0.92.dev109}/CLAUDE.md +0 -0
  25. {flowra-0.0.91 → flowra-0.0.92.dev109}/LICENSE +0 -0
  26. {flowra-0.0.91 → flowra-0.0.92.dev109}/Makefile +0 -0
  27. {flowra-0.0.91 → flowra-0.0.92.dev109}/README.md +0 -0
  28. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/architecture.md +0 -0
  29. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/getting-started.md +0 -0
  30. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/llm.md +0 -0
  31. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/patterns.md +0 -0
  32. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/agent_async_init.md +0 -0
  33. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/agent_tool_errors.md +0 -0
  34. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/anthropic_web_search.md +0 -0
  35. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/e2e_security.md +0 -0
  36. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/enum_slot_values.md +0 -0
  37. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/final_tools.md +0 -0
  38. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/langchain_comparison.md +0 -0
  39. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/lifecycle_hooks_and_conversation.md +0 -0
  40. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/llm_retry_backoff.md +0 -0
  41. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/named_scope_lazy_hydration.md +0 -0
  42. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/named_scope_type_registry.md +0 -0
  43. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/named_scopes.md +0 -0
  44. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/pricing_complexity.md +0 -0
  45. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/pydantic_ai_comparison.md +0 -0
  46. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/strands_comparison.md +0 -0
  47. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/tool_error_signals.md +0 -0
  48. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/tool_errors_vs_failures.md +0 -0
  49. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/tool_registry_redesign.md +0 -0
  50. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/tool_search_tool.md +0 -0
  51. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/research/voice_stt.md +0 -0
  52. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step1_structure.md +0 -0
  53. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step2_code_style.md +0 -0
  54. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step3_documentation.md +0 -0
  55. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step4_doc_readability.md +0 -0
  56. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step5_doc_audit.md +0 -0
  57. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/review_prompts/step6_tests.md +0 -0
  58. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/todo.md +0 -0
  59. {flowra-0.0.91 → flowra-0.0.92.dev109}/docs/video_script.md +0 -0
  60. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/TRACING_COMBINATIONS.md +0 -0
  61. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/__init__.py +0 -0
  62. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/agent_as_tool.py +0 -0
  63. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/app_agent.py +0 -0
  64. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/console_chat.py +0 -0
  65. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/__init__.py +0 -0
  66. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/agents_custom.py +0 -0
  67. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/agents_parallel.py +0 -0
  68. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/getting_started_chat.py +0 -0
  69. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/getting_started_streaming.py +0 -0
  70. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/getting_started_tools.py +0 -0
  71. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/llm_streaming.py +0 -0
  72. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/llm_structured_output.py +0 -0
  73. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/docs/tools_service_injection.py +0 -0
  74. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/escalation.py +0 -0
  75. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/llm_logging.py +0 -0
  76. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/llm_routing.py +0 -0
  77. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/menu_agent.py +0 -0
  78. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/menu_agent_class.py +0 -0
  79. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_demo.py +0 -0
  80. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_dual_export_demo.py +0 -0
  81. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_nested_demo.py +0 -0
  82. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_otel_both_demo.py +0 -0
  83. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_otel_nested_demo.py +0 -0
  84. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/mlflow_parallel_demo.py +0 -0
  85. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/model_registry.py +0 -0
  86. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/otel_demo.py +0 -0
  87. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/otel_jaeger_demo.py +0 -0
  88. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/otel_nested_demo.py +0 -0
  89. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/otel_visualize.py +0 -0
  90. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/race.py +0 -0
  91. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/span_crash_demo.py +0 -0
  92. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/span_demo.py +0 -0
  93. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/system_prompt.txt +0 -0
  94. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/tools/__init__.py +0 -0
  95. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/tools/calculator.py +0 -0
  96. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/tools/random_numbers.py +0 -0
  97. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/tools/switch_model.py +0 -0
  98. {flowra-0.0.91 → flowra-0.0.92.dev109}/examples/tui_chat.py +0 -0
  99. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/__init__.py +0 -0
  100. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/_sentinel.py +0 -0
  101. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/_typing.py +0 -0
  102. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/__init__.py +0 -0
  103. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/__init__.py +0 -0
  104. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/_path.py +0 -0
  105. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/actions.py +0 -0
  106. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/agent.py +0 -0
  107. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/agent_arg.py +0 -0
  108. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/__init__.py +0 -0
  109. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/compiler.py +0 -0
  110. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/contract.py +0 -0
  111. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/init_params.py +0 -0
  112. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/instance.py +0 -0
  113. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/step_params.py +0 -0
  114. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/step_validation.py +0 -0
  115. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/steps.py +0 -0
  116. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/compile/type_registry.py +0 -0
  117. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/model.py +0 -0
  118. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/registry.py +0 -0
  119. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/shared_persistent.py +0 -0
  120. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/spawn.py +0 -0
  121. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/step.py +0 -0
  122. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/definition/step_helpers.py +0 -0
  123. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/__init__.py +0 -0
  124. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/context.py +0 -0
  125. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/flowing_registry.py +0 -0
  126. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/hooks.py +0 -0
  127. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/interrupt.py +0 -0
  128. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/interrupt_helpers.py +0 -0
  129. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/flow/timeout.py +0 -0
  130. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/__init__.py +0 -0
  131. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/engine.py +0 -0
  132. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/execution.py +0 -0
  133. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/instance_factory.py +0 -0
  134. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/key_locks.py +0 -0
  135. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/runtime.py +0 -0
  136. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/scope.py +0 -0
  137. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/serialization.py +0 -0
  138. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/spans.py +0 -0
  139. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/spawn_satisfaction.py +0 -0
  140. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/spawn_tree.py +0 -0
  141. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/runtime/visibility.py +0 -0
  142. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/services.py +0 -0
  143. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/session_state.py +0 -0
  144. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/state/__init__.py +0 -0
  145. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/state/markers.py +0 -0
  146. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/state/persistent.py +0 -0
  147. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/state/store.py +0 -0
  148. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/state/values.py +0 -0
  149. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/storage/__init__.py +0 -0
  150. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/storage/file.py +0 -0
  151. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/storage/in_memory.py +0 -0
  152. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/agent/storage/session_storage.py +0 -0
  153. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/ext/__init__.py +0 -0
  154. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/ext/_session.py +0 -0
  155. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/ext/mlflow.py +0 -0
  156. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/ext/otel.py +0 -0
  157. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/__init__.py +0 -0
  158. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/__init__.py +0 -0
  159. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/agent.py +0 -0
  160. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/config.py +0 -0
  161. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/conversation.py +0 -0
  162. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/hook_events.py +0 -0
  163. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/chat/spec.py +0 -0
  164. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/config_value.py +0 -0
  165. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_call/__init__.py +0 -0
  166. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_call/agent.py +0 -0
  167. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_call/config.py +0 -0
  168. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_call/spec.py +0 -0
  169. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_caller.py +0 -0
  170. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/llm_config.py +0 -0
  171. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/observability/__init__.py +0 -0
  172. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/observability/llm_hooks.py +0 -0
  173. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/config.py +0 -0
  174. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/context.py +0 -0
  175. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/spec.py +0 -0
  176. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/tool_call/__init__.py +0 -0
  177. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/tool_call/agent_tool.py +0 -0
  178. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/lib/turn/tool_call/context.py +0 -0
  179. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/__init__.py +0 -0
  180. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/_schema_nodes.py +0 -0
  181. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/base.py +0 -0
  182. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/blocks.py +0 -0
  183. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/errors.py +0 -0
  184. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/__init__.py +0 -0
  185. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/anthropic/__init__.py +0 -0
  186. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/anthropic/cache.py +0 -0
  187. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/anthropic/presets.py +0 -0
  188. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/anthropic/tool_search.py +0 -0
  189. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/anthropic/web_search.py +0 -0
  190. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/openai/__init__.py +0 -0
  191. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/ext/openai/tool_search.py +0 -0
  192. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/gemini_schema.py +0 -0
  193. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/messages.py +0 -0
  194. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/openai_schema.py +0 -0
  195. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/__init__.py +0 -0
  196. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/bundled.py +0 -0
  197. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/custom/anthropic.json +0 -0
  198. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/custom/vertex.json +0 -0
  199. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/anthropic.json +0 -0
  200. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/azure_openai.json +0 -0
  201. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/bedrock.json +0 -0
  202. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/gemini_api.json +0 -0
  203. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/inception.json +0 -0
  204. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/openai.json +0 -0
  205. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/data/generated/vertex.json +0 -0
  206. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/model_pricing.py +0 -0
  207. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/pricing/price_list.py +0 -0
  208. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/provider.py +0 -0
  209. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/__init__.py +0 -0
  210. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/_openai_common.py +0 -0
  211. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/anthropic.py +0 -0
  212. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/anthropic_bedrock.py +0 -0
  213. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/anthropic_foundry.py +0 -0
  214. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/anthropic_vertex.py +0 -0
  215. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/azure_openai.py +0 -0
  216. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/google_vertex.py +0 -0
  217. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/openai.py +0 -0
  218. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/providers/openai_responses.py +0 -0
  219. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/request.py +0 -0
  220. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/response.py +0 -0
  221. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/schema_formatting.py +0 -0
  222. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/schema_validation.py +0 -0
  223. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/stream.py +0 -0
  224. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/llm/tools.py +0 -0
  225. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/py.typed +0 -0
  226. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/__init__.py +0 -0
  227. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/_mcp_content.py +0 -0
  228. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/_mcp_headers.py +0 -0
  229. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/local_tool.py +0 -0
  230. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/mcp_connection.py +0 -0
  231. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/result_format.py +0 -0
  232. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/tool_arg.py +0 -0
  233. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/tool_filter.py +0 -0
  234. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/tool_registry.py +0 -0
  235. {flowra-0.0.91 → flowra-0.0.92.dev109}/flowra/tools/types.py +0 -0
  236. {flowra-0.0.91 → flowra-0.0.92.dev109}/pyproject.toml +0 -0
  237. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/__init__.py +0 -0
  238. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/__init__.py +0 -0
  239. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/__init__.py +0 -0
  240. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/compile/__init__.py +0 -0
  241. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/compile/test_compile.py +0 -0
  242. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/test_actions.py +0 -0
  243. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/test_agent.py +0 -0
  244. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/test_registry.py +0 -0
  245. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/test_spawn.py +0 -0
  246. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/definition/test_step_helpers.py +0 -0
  247. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/__init__.py +0 -0
  248. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_context.py +0 -0
  249. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_flowing_registry.py +0 -0
  250. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_flowing_registry_tasks.py +0 -0
  251. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_flowing_sync.py +0 -0
  252. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_hooks.py +0 -0
  253. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_interrupt.py +0 -0
  254. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_interruptible.py +0 -0
  255. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_spans.py +0 -0
  256. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_timeout.py +0 -0
  257. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/flow/test_with_interrupt.py +0 -0
  258. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/__init__.py +0 -0
  259. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_engine.py +0 -0
  260. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_engine_spans.py +0 -0
  261. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_ephemeral_isolation.py +0 -0
  262. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_hook_context.py +0 -0
  263. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_named_scope_persistence.py +0 -0
  264. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_named_scope_registry.py +0 -0
  265. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_named_scopes.py +0 -0
  266. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_parallel_failure.py +0 -0
  267. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_persistence.py +0 -0
  268. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_primitive_join.py +0 -0
  269. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_runtime.py +0 -0
  270. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_scope.py +0 -0
  271. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_serialization.py +0 -0
  272. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/runtime/test_spec_in_constructor.py +0 -0
  273. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/state/__init__.py +0 -0
  274. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/state/test_values.py +0 -0
  275. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/storage/__init__.py +0 -0
  276. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/storage/test_file.py +0 -0
  277. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/storage/test_in_memory.py +0 -0
  278. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/test_missing_scenarios.py +0 -0
  279. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/agent/test_session_state.py +0 -0
  280. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/examples/__init__.py +0 -0
  281. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/examples/test_menu_agent.py +0 -0
  282. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/ext/__init__.py +0 -0
  283. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/ext/_mlflow.py +0 -0
  284. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/ext/test_mlflow.py +0 -0
  285. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/ext/test_mlflow_cross_task.py +0 -0
  286. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/ext/test_otel.py +0 -0
  287. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/__init__.py +0 -0
  288. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/_helpers.py +0 -0
  289. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/ext/__init__.py +0 -0
  290. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/ext/anthropic/__init__.py +0 -0
  291. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/ext/anthropic/test_anthropic.py +0 -0
  292. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/ext/openai/__init__.py +0 -0
  293. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/ext/openai/test_openai_tool_search.py +0 -0
  294. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_chat_agent.py +0 -0
  295. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_chat_spec.py +0 -0
  296. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_config_value.py +0 -0
  297. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_conversation.py +0 -0
  298. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_llm_call_agent.py +0 -0
  299. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_llm_caller.py +0 -0
  300. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_matches_tool_filter.py +0 -0
  301. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/test_tool_call_agent_call_agent.py +0 -0
  302. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/turn/__init__.py +0 -0
  303. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/turn/test_per_call_headers.py +0 -0
  304. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/turn/test_render.py +0 -0
  305. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/lib/turn/test_tool_errors.py +0 -0
  306. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/__init__.py +0 -0
  307. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/pricing/__init__.py +0 -0
  308. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/pricing/test_bundled.py +0 -0
  309. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/pricing/test_model_pricing.py +0 -0
  310. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/pricing/test_price_list.py +0 -0
  311. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/__init__.py +0 -0
  312. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/_mock_http.py +0 -0
  313. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/_raw_body.py +0 -0
  314. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/_tool_result_blocks.py +0 -0
  315. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic.py +0 -0
  316. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_bedrock.py +0 -0
  317. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_bedrock_e2e.py +0 -0
  318. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_e2e.py +0 -0
  319. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_foundry.py +0 -0
  320. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_foundry_e2e.py +0 -0
  321. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_vertex.py +0 -0
  322. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_anthropic_vertex_locations_e2e.py +0 -0
  323. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_azure_openai_e2e.py +0 -0
  324. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_cross_provider_e2e.py +0 -0
  325. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_errors.py +0 -0
  326. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_fallback_e2e.py +0 -0
  327. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_google_vertex.py +0 -0
  328. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_google_vertex_e2e.py +0 -0
  329. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_history_across_models.py +0 -0
  330. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_openai_e2e.py +0 -0
  331. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_openai_provider.py +0 -0
  332. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_openai_responses.py +0 -0
  333. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_openai_responses_e2e.py +0 -0
  334. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_refusals.py +0 -0
  335. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/providers/test_schemas_e2e.py +0 -0
  336. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_blocks.py +0 -0
  337. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_cost_breakdown.py +0 -0
  338. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_gemini_schema.py +0 -0
  339. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_metadata.py +0 -0
  340. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_openai_schema.py +0 -0
  341. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_response.py +0 -0
  342. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_schema_formatting.py +0 -0
  343. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_schema_nodes.py +0 -0
  344. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_schema_validation.py +0 -0
  345. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/llm/test_stream.py +0 -0
  346. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/scripts/__init__.py +0 -0
  347. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/scripts/test_sync_pricing.py +0 -0
  348. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/test_typing.py +0 -0
  349. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/__init__.py +0 -0
  350. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_local_tool.py +0 -0
  351. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_mcp_connection.py +0 -0
  352. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_mcp_headers.py +0 -0
  353. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_result_format.py +0 -0
  354. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_tool_registry.py +0 -0
  355. {flowra-0.0.91 → flowra-0.0.92.dev109}/tests/tools/test_types.py +0 -0
  356. {flowra-0.0.91 → flowra-0.0.92.dev109}/tools/sync_pricing.py +0 -0
  357. {flowra-0.0.91 → flowra-0.0.92.dev109}/uv.lock +0 -0
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - **`BeforeToolCallsEvent`: answer a model message's tool calls before any
12
+ runs.** Emitted once per model message with tool calls, a single call
13
+ included, with every call of the message in `tool_uses`, a call the response cut off
14
+ included (an answer under its id is not used). A handler puts a
15
+ `ToolResult` into `results` under a call's id to answer that call itself:
16
+ it does not run, the other calls run as usual, and its result reaches the
17
+ model in the message's order. An answered call gets no
18
+ `BeforeToolCallEvent` and no `ToolCallSpan`; it goes through
19
+ `AfterToolCallEvent` and counts toward `max_consecutive_errors` like any
20
+ result. An error answered without an `error_kind` gets
21
+ `ToolErrorKind.EXECUTION`; a key that is not the id of a call in the message
22
+ raises `ValueError`. Answered calls are kept in the turn's state, so they
23
+ survive a resume before the results are collected.
24
+ - **`AfterToolCallEvent.prefilled`**: true for a call answered in
25
+ `BeforeToolCallsEvent`, so a handler can tell that the tool did not run.
26
+
27
+ ### Changed
28
+ - **A call's answer carries an `error_kind` exactly when it is an error.**
29
+ `ToolCallResult` sets `ToolErrorKind.EXECUTION` for an error that came
30
+ without a kind — a `ToolResult(is_error=True)` a tool or an agent tool
31
+ returns, "Tool execution interrupted" — and drops a kind from an answer that
32
+ is not an error. `AfterToolCallEvent.error_kind` and `ToolCallSpan.error_kind`
33
+ follow, so a consumer telling an error by its kind no longer reads such an
34
+ error as a success.
35
+
8
36
  ## [0.0.91] - 2026-10-20
9
37
 
10
38
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: flowra
3
- Version: 0.0.91
3
+ Version: 0.0.92.dev109
4
4
  Summary: Flowra — flow infrastructure for building stateful LLM agents
5
5
  Project-URL: Repository, https://github.com/anna-money/flowra
6
6
  Project-URL: Changelog, https://github.com/anna-money/flowra/blob/master/CHANGELOG.md
@@ -48,7 +48,7 @@
48
48
  "McpConnection connects to remote MCP servers for tool discovery over Streamable HTTP. McpConnection.snapshot() returns a frozen ToolRegistry; the tool list is refreshed in the background (server ttlMs, 60s default) with add_on_tools_changed(callback) for changes, and tools with invalid x-mcp-header annotations are dropped. Speaks both MCP protocol eras and detects on connect: stateless 2026-07-28 (no handshake/session; x-mcp-header args mirrored into Mcp-Param-* headers) and handshake-based 2025-03-26 to 2025-11-25 (any other version fails connect()). McpConnection.session (McpSession) exposes stateless: bool, protocol_version and session_id. A tool call needing client input (MRTR) returns a tool error; subscriptions/listen and MRTR are not implemented. McpConnection.call_tool(name, args, headers=..., meta=...) adds per-call HTTP headers and params._meta (kept on the reconnect retry; Content-Type/Accept/Mcp-* headers and the protocol's io.modelcontextprotocol/* _meta keys raise ValueError)",
49
49
  "ToolCallInfo (flowra.tools) carries per-call headers and meta, empty by default — flowra sets none itself. Fill them in a BeforeToolCallEvent handler (event.data.headers / event.data.meta); they are stored in ToolCallSpec, so a resumed call gets the same values (generate idempotency keys there, not in the tool). ToolCallAgent passes ToolCallInfo in services: MCP snapshot tools send them with tools/call, a @tool reads them by declaring an info: ToolCallInfo parameter",
50
50
  "ToolRegistry is immutable, accepts @tool-decorated functions or ExecutableTools: ToolRegistry([my_func, other_func])",
51
- "TOOL ERRORS: the executor built by @tool catches the decorated function's exceptions (and argument validation/service binding errors) and returns ToolResult(content='Error: {e}', is_error=True, error_kind=EXECUTION, or SCHEMA_VALIDATION for invalid arguments) — so registry.execute and a direct get_tool(f).executor(...) call both return the error result. A ToolResult the function returns is the answer as it is (not serialized); ToolResult.content is str | list[TextBlock | MediaBlock], and ToolCallAgent puts its blocks into the ToolResultBlock as they are (metadata and transient kept; a str is one TextBlock; ToolResult.blocks / ToolResult.text). A '-> ToolResult' annotation gives no output_schema; '-> Answer | ToolResult' gives Answer's. raise ToolError(content, error_kind=EXECUTION) (str or blocks) to answer with an error the tool chose: the executor returns exc.result (is_error=True, content as written, no 'Error: ' prefix, no log); only the @tool executor answers it — an agent behind an agent tool returns ToolResult(is_error=True) instead, and a ToolError raised in an agent fails the run like any exception. InterruptedError is the exception: the executor raises it, and ToolCallAgent answers 'Tool execution interrupted'. ToolRegistry.execute does not catch: an exception from code around an executor (a wrapper via dataclasses.replace(tool, executor=...), a custom ExecutableTool executor) propagates through ToolCallAgent to AgentRuntime.run/resume as itself (session stays resumable); return ToolResult(is_error=True) from such code to answer the model. ToolCallAgent answers InterruptedError raised around a tool with 'Tool execution interrupted'. McpConnection never raises for a call, except ValueError for invalid per-call headers/_meta (set by a BeforeToolCallEvent handler, it fails the run and a resume repeats it, since the values are stored with the call): unknown/removed tools → UNKNOWN_TOOL; a result's MCP content comes back as blocks (text and resource text → TextBlock; image, audio and resource blob → MediaBlock; resource_link and unknown types → TextBlock with the block as JSON), with what the flowra block does not carry in metadata['mcp'] (text/image/audio: every field but type, content (text/data) and, for media, mimeType (annotations, _meta, any other), no metadata when none, so plain text is a plain TextBlock; resource: the whole block but the resource's text/blob, uri and mimeType under metadata['mcp']['resource']; resource_link and unknown types: type, annotations, _meta); a malformed block makes the response invalid; isError → the server's blocks, JSON-RPC errors → the server's text; transport failures → 'temporarily unavailable' (retried once if the reconnect succeeds); invalid responses → 'returned an invalid response', not retried. When one of several parallel steps (e.g. parallel tool calls) raises, the engine cancels and awaits its siblings, then re-raises the original exception (no ExceptionGroup). Agent tool executors (make_agent_tool/@agent_tool) answer invalid arguments with SCHEMA_VALIDATION/EXECUTION results like @tool; with valid arguments, one called outside ToolCallAgent raises RuntimeError",
51
+ "TOOL ERRORS: the executor built by @tool catches the decorated function's exceptions (and argument validation/service binding errors) and returns ToolResult(content='Error: {e}', is_error=True, error_kind=EXECUTION, or SCHEMA_VALIDATION for invalid arguments) — so registry.execute and a direct get_tool(f).executor(...) call both return the error result. A ToolResult the function returns is the answer as it is (not serialized), except that what hooks and spans see (ToolCallResult, AfterToolCallEvent, ToolCallSpan) carries an error_kind exactly when it is an error — EXECUTION for an error given without one, None for a non-error; ToolResult.content is str | list[TextBlock | MediaBlock], and ToolCallAgent puts its blocks into the ToolResultBlock as they are (metadata and transient kept; a str is one TextBlock; ToolResult.blocks / ToolResult.text). A '-> ToolResult' annotation gives no output_schema; '-> Answer | ToolResult' gives Answer's. raise ToolError(content, error_kind=EXECUTION) (str or blocks) to answer with an error the tool chose: the executor returns exc.result (is_error=True, content as written, no 'Error: ' prefix, no log); only the @tool executor answers it — an agent behind an agent tool returns ToolResult(is_error=True) instead, and a ToolError raised in an agent fails the run like any exception. InterruptedError is the exception: the executor raises it, and ToolCallAgent answers 'Tool execution interrupted'. ToolRegistry.execute does not catch: an exception from code around an executor (a wrapper via dataclasses.replace(tool, executor=...), a custom ExecutableTool executor) propagates through ToolCallAgent to AgentRuntime.run/resume as itself (session stays resumable); return ToolResult(is_error=True) from such code to answer the model. ToolCallAgent answers InterruptedError raised around a tool with 'Tool execution interrupted'. McpConnection never raises for a call, except ValueError for invalid per-call headers/_meta (set by a BeforeToolCallEvent handler, it fails the run and a resume repeats it, since the values are stored with the call): unknown/removed tools → UNKNOWN_TOOL; a result's MCP content comes back as blocks (text and resource text → TextBlock; image, audio and resource blob → MediaBlock; resource_link and unknown types → TextBlock with the block as JSON), with what the flowra block does not carry in metadata['mcp'] (text/image/audio: every field but type, content (text/data) and, for media, mimeType (annotations, _meta, any other), no metadata when none, so plain text is a plain TextBlock; resource: the whole block but the resource's text/blob, uri and mimeType under metadata['mcp']['resource']; resource_link and unknown types: type, annotations, _meta); a malformed block makes the response invalid; isError → the server's blocks, JSON-RPC errors → the server's text; transport failures → 'temporarily unavailable' (retried once if the reconnect succeeds); invalid responses → 'returned an invalid response', not retried. When one of several parallel steps (e.g. parallel tool calls) raises, the engine cancels and awaits its siblings, then re-raises the original exception (no ExceptionGroup). Agent tool executors (make_agent_tool/@agent_tool) answer invalid arguments with SCHEMA_VALIDATION/EXECUTION results like @tool; with valid arguments, one called outside ToolCallAgent raises RuntimeError",
52
52
  "ToolRegistry.builder() returns ToolRegistryBuilder with .add(source, prefix=..., filter=..., on_duplicate='error'|'skip') and .build() for composing registries from multiple sources. filter is a single Callable[[ExecutableTool], bool]; use the `tool_filter` module for common cases: tool_filter.INCLUDE_SUFFIX(*names) whitelists, tool_filter.EXCLUDE_SUFFIX(*names) blacklists (both match exact name or __name suffix), tool_filter.ALL_OF(*filters)/tool_filter.ANY_OF(*filters) compose. Import: from flowra.tools import tool_filter",
53
53
  "Duplicate tool names are detected eagerly at the ToolRegistryBuilder.add() call where the collision is introduced. Default on_duplicate='error' raises ValueError; on_duplicate='skip' silently drops repeats (first occurrence wins). ToolRegistry(...) constructor also raises on duplicate names. OnDuplicate type alias is exported from flowra.tools",
54
54
  "ToolRegistry is a ConfigValue[ToolRegistry | None] in ChatConfig and TurnConfig (field name: tools)",
@@ -129,12 +129,12 @@
129
129
 
130
130
  "PRE-BUILT AGENTS — TurnAgent: single-turn LLM tool loop with hooks and caching",
131
131
  "TurnAgent sends messages to LLM, executes tool calls, feeds results back, repeats until done",
132
- "TurnAgent stop handling: a response without tool calls ends the turn with TurnStatus.COMPLETED and TurnResult.stop_reason (END_TURN, MAX_TOKENS, STOP_SEQUENCE, OTHER, SCHEMA_VALIDATION_FAILED, REFUSAL). On REFUSAL TurnResult.refusal is set, result_message is None and messages keep the turn so far (tool calls included) with no empty assistant message. If a response stopped with anything but END_TURN/TOOL_USE ends on a tool call, that call was cut off: it is not run and gets an error ToolResultBlock (no Before/AfterToolCallEvent, no ToolCallSpan, not counted in max_consecutive_errors), the complete calls before it run, and the loop continues so the model can retry (bounded by max_llm_calls). History never holds an unanswered tool_use",
132
+ "TurnAgent stop handling: a response without tool calls ends the turn with TurnStatus.COMPLETED and TurnResult.stop_reason (END_TURN, MAX_TOKENS, STOP_SEQUENCE, OTHER, SCHEMA_VALIDATION_FAILED, REFUSAL). On REFUSAL TurnResult.refusal is set, result_message is None and messages keep the turn so far (tool calls included) with no empty assistant message. If a response stopped with anything but END_TURN/TOOL_USE ends on a tool call, that call was cut off: it is not run and gets an error ToolResultBlock (it appears in BeforeToolCallsEvent.tool_uses; no BeforeToolCallEvent/AfterToolCallEvent, no ToolCallSpan, not counted in max_consecutive_errors), the complete calls before it run, and the loop continues so the model can retry (bounded by max_llm_calls). History never holds an unanswered tool_use",
133
133
  "TurnConfig configures TurnAgent: TurnConfig(llm_config=LLMConfig(model='...'), system=[...], history=[...])",
134
134
  "RENDER: ChatConfig.render / TurnConfig.render (PromptRender = Callable[[Prompt], Prompt | Awaitable[Prompt]]; Prompt from flowra.lib.turn has system and messages) is called once per LLM call of the turn (not per fallback attempt), before LLMCaller, with the system messages and the conversation (history, then turn messages). What it returns is sent to every fallback link; each link's request_extensions apply after it, and each attempt's LLMCallSpan carries the request with them applied. Stored history and turn messages are not changed, so a marker block can be rendered from live state on each call; build new messages, never edit the given ones. Messages left with no blocks are not sent, none left at all is a ValueError before the call; a tool call must still be followed straight away by its results (Claude and OpenAI Chat Completions reject anything in between); an error in render is raised from the turn as is; an interrupt stops an async render and ends the turn INTERRUPTED",
135
135
  "TurnAgent lifecycle: __post_init__ emits TurnReadyEvent(context, turn_messages) — fires on both creation and crash recovery. ChatAgent subscribes and emits ConversationReadyEvent(conversation, context) with a Conversation view combining history + turn messages",
136
136
  "Conversation class (from flowra.lib.chat): read-only Sequence view over history + turn_messages. Supports len(), indexing, slicing, forward/reverse iteration. Live — reflects changes to underlying lists. Properties: history_len, turn_len. Method without_pending_tool_use() -> list[UserMessage | AssistantMessage] returns messages with ToolUseBlocks stripped from the last AssistantMessage (drops the message entirely if it becomes empty) — use it inside an agent-as-tool to feed the parent conversation to a child LLM call without the unresolved tool-use that spawned it",
137
- "TurnAgent hook events: StartTurnEvent (messages, metadata — mutate messages to augment input), MessageAcceptedEvent, StartIterationEvent, TextReasoningEvent (each TextBlock with text in response), ThinkingEvent (each ThinkingBlock with text; not redacted or signature-only thoughts), ServerToolCallEvent (server-side tool call from intermediate messages, e.g. tool search), ServerToolResultEvent (server-side tool result), ResultMessageEvent, BeforeToolCallEvent (has tool_definition: ToolDefinition | None; mutable tool_use, headers, meta — see ToolCallInfo), AfterToolCallEvent (has error_kind; result is a single ToolResultBlock; append TextBlock/MediaBlock to additional_blocks to attach them to this call's result — they go at the end of its content, also when max_consecutive_errors replaces it, so they stay with their call and never land between the results of parallel calls; mark one transient to keep it out of saved history; being part of the result, a MediaBlock there takes the providers' rules for media in a tool result (Chat Completions raises, Gemini before 3 does not see it) and a transient block ends the non-transient cache prefix before the result). Delta events: TextDeltaEvent, ThinkingDeltaEvent (from flowra.lib.observability — shared with LLMCallAgent). UsageEvent emitted after each LLM call + subscribed to accumulate usage from child agents (e.g. agent_as_tool). Span-hooks: LLMCallSpan, ToolCallSpan. Request transforms (prompt caching, tool search) are NOT hooks — they live on LLMConfig.request_extensions. A conversation transform for every fallback link goes in ChatConfig.render / TurnConfig.render (see RENDER)",
137
+ "TurnAgent hook events: StartTurnEvent (messages, metadata — mutate messages to augment input), MessageAcceptedEvent, StartIterationEvent, TextReasoningEvent (each TextBlock with text in response), ThinkingEvent (each ThinkingBlock with text; not redacted or signature-only thoughts), ServerToolCallEvent (server-side tool call from intermediate messages, e.g. tool search), ServerToolResultEvent (server-side tool result), ResultMessageEvent, BeforeToolCallsEvent (once per model message with tool calls, before any runs; tool_uses is every call of the message, a cut-off call included (an answer under its id is not used); put a ToolResult into results under a call's id to answer it without running it — that call gets no BeforeToolCallEvent and no ToolCallSpan, its result goes through AfterToolCallEvent with prefilled=True and max_consecutive_errors in message order, an error without error_kind gets EXECUTION; a key not among the message's call ids raises ValueError), BeforeToolCallEvent (has tool_definition: ToolDefinition | None; mutable tool_use, headers, meta — see ToolCallInfo; not emitted for a call answered in BeforeToolCallsEvent), AfterToolCallEvent (fires after execution, and for a call answered in BeforeToolCallsEvent; has error_kind; prefilled is true for a call answered in BeforeToolCallsEvent; result is a single ToolResultBlock; append TextBlock/MediaBlock to additional_blocks to attach them to this call's result — they go at the end of its content, also when max_consecutive_errors replaces it, so they stay with their call and never land between the results of parallel calls; mark one transient to keep it out of saved history; being part of the result, a MediaBlock there takes the providers' rules for media in a tool result (Chat Completions raises, Gemini before 3 does not see it) and a transient block ends the non-transient cache prefix before the result). Delta events: TextDeltaEvent, ThinkingDeltaEvent (from flowra.lib.observability — shared with LLMCallAgent). UsageEvent emitted after each LLM call + subscribed to accumulate usage from child agents (e.g. agent_as_tool). Span-hooks: LLMCallSpan, ToolCallSpan. Request transforms (prompt caching, tool search) are NOT hooks — they live on LLMConfig.request_extensions. A conversation transform for every fallback link goes in ChatConfig.render / TurnConfig.render (see RENDER)",
138
138
  "When TextDeltaEvent or ThinkingDeltaEvent handlers are registered, TurnAgent automatically uses provider.stream() instead of provider.call()",
139
139
 
140
140
  "AGENTS AS TOOLS: Tools can delegate to sub-agents via ToolCallAgentContext.call_agent(). make_agent_tool(AgentClass, name, description) creates an ExecutableTool with input_schema from spec type and output_schema from result type (ToolResult excluded). @agent_tool decorator marks an agent class as a tool, get_agent_tool() extracts the ExecutableTool. ToolCallAgent spawns the sub-agent and serializes its result as a ToolResultBlock. Agent tools can return ToolResult directly to signal errors to the parent LLM or answer with blocks (e.g. return ToolResult(content='...', is_error=True)) — the agent completes normally; with is_error=True the parent LLM sees a tool error and can retry. Compile-time validation: step return types must be a subset of Agent's generic result type parameter. The sub-agent must be declared by the agent running the tool or one of its registry ancestors (top level works); it receives services of its own registry ancestors, not the calling chat's (unless the chat is one of them)",
@@ -802,11 +802,14 @@ Lifecycle events (from `flowra.lib.turn`):
802
802
  | `ThinkingEvent` | Non-empty thinking block in LLM response | — |
803
803
  | `ServerToolCallEvent` | Server-side tool call (e.g. tool search) | — |
804
804
  | `ServerToolResultEvent` | Server-side tool result | — |
805
+ | `BeforeToolCallsEvent` | Message with tool calls, before any runs | `results` |
805
806
  | `BeforeToolCallEvent` | Before tool execution | `tool_use`, `headers`, `meta` |
806
- | `AfterToolCallEvent` | After tool execution | `result`, `additional_blocks`, `additional_messages` |
807
+ | `AfterToolCallEvent` | After tool execution or answer | `result`, `additional_blocks`, `additional_messages` |
807
808
  | `ResultMessageEvent` | LLM finished (END_TURN) | `continue_messages` |
808
809
 
809
- Tool calls cut off by the output limit fire no tool events; see
810
+ A tool call cut off by the output limit appears in `BeforeToolCallsEvent` but
811
+ fires no `BeforeToolCallEvent` or `AfterToolCallEvent`, and a call answered in
812
+ `BeforeToolCallsEvent` does not run; see
810
813
  [Pre-built events](observability.md#pre-built-events) in Observability.
811
814
 
812
815
  Streaming events (from `flowra.lib.observability`):
@@ -35,8 +35,9 @@ Events from `TurnAgent` (and `ChatAgent`, which uses it internally):
35
35
  | `ThinkingEvent` | Non-empty thinking block in LLM response | — |
36
36
  | `ServerToolCallEvent` | Server-side tool call (e.g. tool search) | — |
37
37
  | `ServerToolResultEvent` | Server-side tool result | — |
38
+ | `BeforeToolCallsEvent` | Message with tool calls, before any runs | `results` |
38
39
  | `BeforeToolCallEvent` | Before tool execution | `tool_use`, `headers`, `meta` |
39
- | `AfterToolCallEvent` | After tool execution | `result`, `additional_blocks`, `additional_messages` |
40
+ | `AfterToolCallEvent` | After tool execution or answer | `result`, `additional_blocks`, `additional_messages` |
40
41
  | `ResultMessageEvent` | LLM finished (END_TURN) | `continue_messages` |
41
42
 
42
43
  `AfterToolCallEvent.additional_blocks` go at the end of that call's result, so a
@@ -71,7 +72,37 @@ runtime.hooks.on(AfterToolCallEvent, add_hint)
71
72
  A tool call cut off by the output limit (see [TurnAgent](agents.md#turnagent)) is
72
73
  never executed. It gets an error result directly, with no `BeforeToolCallEvent`,
73
74
  `AfterToolCallEvent` or `ToolCallSpan`, so hooks that audit or rewrite tool
74
- results don't see it.
75
+ results don't see it; it does appear in `BeforeToolCallsEvent.tool_uses`, and an
76
+ answer put under its id is not used.
77
+
78
+ `BeforeToolCallsEvent` fires once per model message with tool calls, a single
79
+ call included, before any of them runs. A handler answers a call itself by
80
+ putting a `ToolResult` into `results` under the call's id: that call does not
81
+ run, and the others run as usual. An answered call gets no `BeforeToolCallEvent`
82
+ and no `ToolCallSpan`; its result goes through `AfterToolCallEvent` (with
83
+ `prefilled=True`) and `max_consecutive_errors` like any other, in the message's
84
+ order. An error answered without an `error_kind` arrives with
85
+ `ToolErrorKind.EXECUTION`, as any error a tool gives without one does. A key that is not the id of a call in the message
86
+ fails the step with `ValueError`.
87
+
88
+ ```python
89
+ from flowra.agent import Event
90
+ from flowra.lib.turn import BeforeToolCallsEvent
91
+ from flowra.tools import ToolResult
92
+
93
+
94
+ def reply_alone(event: Event[BeforeToolCallsEvent]) -> None:
95
+ if len(event.data.tool_uses) < 2:
96
+ return
97
+ for tool_use in event.data.tool_uses:
98
+ if tool_use.name == "send_reply":
99
+ event.data.results[tool_use.id] = ToolResult(
100
+ content="Call send_reply on its own, in a message with no other tool calls.", is_error=True
101
+ )
102
+
103
+
104
+ runtime.hooks.on(BeforeToolCallsEvent, reply_alone)
105
+ ```
75
106
 
76
107
  A tool call that fails the run (an exception raised around a tool's executor,
77
108
  see [Error handling](tools.md#what-fails-the-run)) has no result:
@@ -684,8 +684,9 @@ async def respond(params: RespondParams) -> Sent:
684
684
  `ToolError(content, error_kind=ToolErrorKind.EXECUTION)` takes a string or a
685
685
  list of blocks; `exc.result` is the `ToolResult` it stands for. Returning
686
686
  `ToolResult(content=..., is_error=True, error_kind=ToolErrorKind.EXECUTION)`
687
- gives the same answer (without `error_kind`, the model sees the same, but hooks
688
- and spans get `None`).
687
+ gives the same answer. Without `error_kind`, an error still reaches hooks and
688
+ spans as `EXECUTION`: a call's answer carries a kind exactly when it is an
689
+ error, so `error_kind is not None` and `is_error` never disagree.
689
690
 
690
691
  `ToolError` is for `@tool` functions: only their executor answers it. An agent
691
692
  behind an [agent tool](#agents-as-tools) returns its error as a `ToolResult`
@@ -4,6 +4,7 @@ from .context import TurnAgentContext
4
4
  from .hook_events import (
5
5
  AfterToolCallEvent,
6
6
  BeforeToolCallEvent,
7
+ BeforeToolCallsEvent,
7
8
  MessageAcceptedEvent,
8
9
  ResultMessageEvent,
9
10
  ServerToolCallEvent,
@@ -20,6 +21,7 @@ from .tool_call import ToolCallAgentContext, agent_tool, get_agent_tool, make_ag
20
21
  __all__ = [
21
22
  "AfterToolCallEvent",
22
23
  "BeforeToolCallEvent",
24
+ "BeforeToolCallsEvent",
23
25
  "MessageAcceptedEvent",
24
26
  "Prompt",
25
27
  "PromptRender",
@@ -36,7 +36,7 @@ from ...llm import (
36
36
  Usage,
37
37
  UserMessage,
38
38
  )
39
- from ...tools import ToolRegistry
39
+ from ...tools import ToolRegistry, ToolResult
40
40
  from ..config_value import resolve_config_value
41
41
  from ..llm_caller import DEFAULT_LLM_CALLER, LLMCaller
42
42
  from ..observability import UsageEvent
@@ -45,6 +45,7 @@ from .context import TurnAgentContext
45
45
  from .hook_events import (
46
46
  AfterToolCallEvent,
47
47
  BeforeToolCallEvent,
48
+ BeforeToolCallsEvent,
48
49
  MessageAcceptedEvent,
49
50
  ResultMessageEvent,
50
51
  ServerToolCallEvent,
@@ -65,6 +66,8 @@ logger = logging.getLogger(__name__)
65
66
 
66
67
  class TurnAgent(Agent[TurnSpec, TurnResult]):
67
68
  __slots__ = (
69
+ "__answered_tool_results",
70
+ "__call_order",
68
71
  "__caller",
69
72
  "__config",
70
73
  "__consecutive_errors",
@@ -97,6 +100,8 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
97
100
  self.__usage = store.ephemeral.slot(Scalar[Usage | None], "usage")
98
101
  self.__consecutive_errors = store.ephemeral.slot(Scalar[dict[str, int]], "consecutive_errors")
99
102
  self.__incomplete_tool_result = store.ephemeral.slot(Scalar[ToolResultBlock | None], "incomplete_tool_result")
103
+ self.__answered_tool_results = store.ephemeral.slot(Scalar[list[ToolCallResult]], "answered_tool_results")
104
+ self.__call_order = store.ephemeral.slot(Scalar[list[str]], "call_order")
100
105
  self.__interrupt = interrupt
101
106
  self.__context = TurnAgentContext()
102
107
  self.__config = config
@@ -246,7 +251,7 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
246
251
  )
247
252
 
248
253
  case StopReason.TOOL_USE:
249
- return await self.__call_tools(response, tool_uses, registry, allowed_tools, denied_tools)
254
+ return await self.__call_tools(response, tool_uses, None, registry, allowed_tools, denied_tools)
250
255
 
251
256
  case StopReason.REFUSAL:
252
257
  # The provider declined: no output to keep, and an empty assistant message would make
@@ -268,7 +273,7 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
268
273
  # A response cut off mid tool call ends on that call, and only that call is incomplete
269
274
  cut_off = response.message.blocks[-1] is tool_uses[-1]
270
275
  if not cut_off:
271
- return await self.__call_tools(response, tool_uses, registry, allowed_tools, denied_tools)
276
+ return await self.__call_tools(response, tool_uses, None, registry, allowed_tools, denied_tools)
272
277
 
273
278
  # Answer the cut-off call with an error so history stays valid and the model can retry
274
279
  self.__incomplete_tool_result.set(
@@ -278,7 +283,9 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
278
283
  is_error=True,
279
284
  )
280
285
  )
281
- return await self.__call_tools(response, tool_uses[:-1], registry, allowed_tools, denied_tools)
286
+ return await self.__call_tools(
287
+ response, tool_uses[:-1], tool_uses[-1], registry, allowed_tools, denied_tools
288
+ )
282
289
 
283
290
  @step("collect_results")
284
291
  async def collect_results(
@@ -289,13 +296,17 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
289
296
  consecutive_errors = dict(self.__consecutive_errors.get({}))
290
297
  max_consecutive = await resolve_config_value(self.__config.max_consecutive_errors)
291
298
 
292
- for tc_result in results:
299
+ answered = self.__answered_tool_results.get([])
300
+ answered_ids = {tc_result.tool_use.id for tc_result in answered}
301
+ collected = self.__in_call_order([*answered, *results]) if answered else results
302
+ for tc_result in collected:
293
303
  after_event = await self.__hooks.emit(
294
304
  AfterToolCallEvent(
295
305
  tool_use=tc_result.tool_use,
296
306
  result=tc_result.result,
297
307
  error_kind=tc_result.error_kind,
298
308
  context=self.__context,
309
+ prefilled=tc_result.tool_use.id in answered_ids,
299
310
  )
300
311
  )
301
312
 
@@ -332,6 +343,9 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
332
343
  if incomplete_result is not None:
333
344
  result_blocks.append(incomplete_result)
334
345
  self.__incomplete_tool_result.set(None)
346
+ if answered:
347
+ self.__answered_tool_results.set([])
348
+ self.__call_order.set([])
335
349
 
336
350
  self.__consecutive_errors.set(consecutive_errors)
337
351
  user_message = UserMessage(blocks=list(result_blocks))
@@ -370,13 +384,33 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
370
384
  self,
371
385
  response: LLMResponse,
372
386
  tool_uses: list[ToolUseBlock],
387
+ cut_off: ToolUseBlock | None,
373
388
  registry: ToolRegistry | None,
374
389
  allowed_tools: set[str] | None,
375
390
  denied_tools: set[str] | None,
376
391
  ) -> Spawn:
392
+ message_calls = [*tool_uses, cut_off] if cut_off is not None else list(tool_uses)
393
+ # Taken before the emit, so a handler changing the event's list cannot widen what may be answered
394
+ call_ids = {tool_use.id for tool_use in message_calls}
395
+ calls_event = await self.__hooks.emit(BeforeToolCallsEvent(tool_uses=message_calls, context=self.__context))
396
+ for tool_use_id in calls_event.results:
397
+ if tool_use_id not in call_ids:
398
+ raise ValueError(f"No tool call {tool_use_id!r} in this message")
399
+ # Answers are taken from the calls that can run, so the cut-off call keeps its own error result
400
+ answered = [
401
+ self.__answered_result(tool_use, calls_event.results[tool_use.id])
402
+ for tool_use in tool_uses
403
+ if tool_use.id in calls_event.results
404
+ ]
405
+ if answered:
406
+ self.__answered_tool_results.set(answered)
407
+ self.__call_order.set([tool_use.id for tool_use in tool_uses])
408
+
377
409
  amendments: dict[str, ToolUseBlock] = {}
378
410
  final_calls: list[BeforeToolCallEvent] = []
379
411
  for tool_use in tool_uses:
412
+ if tool_use.id in calls_event.results:
413
+ continue
380
414
  before_event = await self.__hooks.emit(
381
415
  BeforeToolCallEvent(
382
416
  tool_use=tool_use,
@@ -413,6 +447,20 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
413
447
  ]
414
448
  return Spawn(children=children, then=self.collect_results)
415
449
 
450
+ def __in_call_order(self, results: list[ToolCallResult]) -> list[ToolCallResult]:
451
+ # By the order the message's calls had when they were made, kept beside the answers: it holds
452
+ # whatever the history looks like by the time the results are collected
453
+ position = {tool_use_id: i for i, tool_use_id in enumerate(self.__call_order.get([]))}
454
+ return sorted(results, key=lambda tc_result: position[tc_result.tool_use.id])
455
+
456
+ @staticmethod
457
+ def __answered_result(tool_use: ToolUseBlock, result: ToolResult) -> ToolCallResult:
458
+ return ToolCallResult(
459
+ tool_use=tool_use,
460
+ result=ToolResultBlock(tool_use_id=tool_use.id, content=result.blocks, is_error=result.is_error),
461
+ error_kind=result.error_kind,
462
+ )
463
+
416
464
  @staticmethod
417
465
  def __incomplete_tool_use_message(stop_reason: StopReason) -> str:
418
466
  if stop_reason == StopReason.MAX_TOKENS:
@@ -14,12 +14,13 @@ from ...llm import (
14
14
  ToolUseBlock,
15
15
  UserMessage,
16
16
  )
17
- from ...tools import ToolDefinition, ToolErrorKind
17
+ from ...tools import ToolDefinition, ToolErrorKind, ToolResult
18
18
  from .context import TurnAgentContext
19
19
 
20
20
  __all__ = [
21
21
  "AfterToolCallEvent",
22
22
  "BeforeToolCallEvent",
23
+ "BeforeToolCallsEvent",
23
24
  "MessageAcceptedEvent",
24
25
  "ResultMessageEvent",
25
26
  "ServerToolCallEvent",
@@ -96,6 +97,21 @@ class ResultMessageEvent:
96
97
  continue_messages: list[UserMessage] = dataclasses.field(default_factory=list)
97
98
 
98
99
 
100
+ @dataclasses.dataclass(slots=True, kw_only=True)
101
+ class BeforeToolCallsEvent:
102
+ """Emitted once per model message with tool calls, before any of them runs.
103
+
104
+ ``tool_uses`` is every call of the message in its order, a call the response cut off included.
105
+ An answer under the cut-off call's id is not used: that call always gets its own error result.
106
+ Put a ``ToolResult`` into ``results`` under a call's id to answer it without running it: the
107
+ other calls run as usual, and the result reaches the model as that call's answer.
108
+ """
109
+
110
+ tool_uses: list[ToolUseBlock]
111
+ context: TurnAgentContext
112
+ results: dict[str, ToolResult] = dataclasses.field(default_factory=dict)
113
+
114
+
99
115
  @dataclasses.dataclass(slots=True, kw_only=True)
100
116
  class BeforeToolCallEvent:
101
117
  """Emitted before each tool execution.
@@ -112,6 +128,9 @@ class BeforeToolCallEvent:
112
128
 
113
129
  Neither this nor ``AfterToolCallEvent`` fires for a call the response cut
114
130
  off (e.g. at ``MAX_TOKENS``): that call is not run.
131
+
132
+ Not emitted for a call answered in ``BeforeToolCallsEvent``; that call still gets
133
+ ``AfterToolCallEvent``, with ``prefilled=True``.
115
134
  """
116
135
 
117
136
  tool_use: ToolUseBlock
@@ -123,7 +142,7 @@ class BeforeToolCallEvent:
123
142
 
124
143
  @dataclasses.dataclass(slots=True, kw_only=True)
125
144
  class AfterToolCallEvent:
126
- """Emitted after each tool execution.
145
+ """Emitted after each tool execution, and for a call answered in ``BeforeToolCallsEvent``.
127
146
 
128
147
  Replace ``result`` to modify tool output (e.g. sanitize errors, redact PII).
129
148
  Append to ``additional_blocks`` to attach blocks (e.g. a hint as a ``TextBlock``)
@@ -136,12 +155,15 @@ class AfterToolCallEvent:
136
155
  this result.
137
156
  Append to ``additional_messages`` to inject standalone messages after the
138
157
  message with the tool results.
158
+
159
+ ``prefilled`` is true for a call answered in ``BeforeToolCallsEvent``: the tool did not run.
139
160
  """
140
161
 
141
162
  tool_use: ToolUseBlock
142
163
  result: ToolResultBlock
143
164
  error_kind: ToolErrorKind | None
144
165
  context: TurnAgentContext
166
+ prefilled: bool = False
145
167
  additional_blocks: list[ToolResultContentBlock] = dataclasses.field(default_factory=list)
146
168
  additional_messages: list[UserMessage] = dataclasses.field(default_factory=list)
147
169
 
@@ -43,10 +43,21 @@ class ToolCallSpec:
43
43
 
44
44
  @dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
45
45
  class ToolCallResult:
46
+ """A call's answer as the turn reads it.
47
+
48
+ ``error_kind`` is set exactly when ``result.is_error``: an error the tool gave no kind is ``EXECUTION``,
49
+ the kind ``ToolError`` defaults to, and an answer that is not an error carries none. Hooks and spans tell
50
+ an error by its kind, so the two never disagree.
51
+ """
52
+
46
53
  tool_use: ToolUseBlock
47
54
  result: ToolResultBlock
48
55
  error_kind: ToolErrorKind | None = None
49
56
 
57
+ def __post_init__(self) -> None:
58
+ error_kind = (self.error_kind or ToolErrorKind.EXECUTION) if self.result.is_error else None
59
+ object.__setattr__(self, "error_kind", error_kind)
60
+
50
61
 
51
62
  class ToolCallAgent(Agent[ToolCallSpec, ToolCallResult]):
52
63
  """Executes a single tool call and returns the result.
@@ -1,4 +1,4 @@
1
1
  # Updated by CI/CD pipeline (the __version__ line only)
2
2
  __all__ = ["__version__"]
3
3
 
4
- __version__ = "0.0.91"
4
+ __version__ = "0.0.92.dev109"
@@ -7,8 +7,8 @@ from flowra.agent import AgentRuntime, InterruptedError
7
7
  from flowra.lib import LLMConfig
8
8
  from flowra.lib.turn import TurnConfig
9
9
  from flowra.lib.turn.tool_call import ToolCallAgent, ToolCallResult, ToolCallSpec
10
- from flowra.llm import LLMProvider, LLMRequest, LLMResponse, ToolUseBlock
11
- from flowra.tools import ToolRegistry, tool
10
+ from flowra.llm import LLMProvider, LLMRequest, LLMResponse, ToolResultBlock, ToolUseBlock
11
+ from flowra.tools import ToolErrorKind, ToolRegistry, ToolResult, tool
12
12
 
13
13
 
14
14
  class _UnusedProvider(LLMProvider):
@@ -37,6 +37,18 @@ def stopped_tool() -> str:
37
37
  raise InterruptedError
38
38
 
39
39
 
40
+ @tool("refusing")
41
+ def refusing_tool() -> ToolResult:
42
+ """Answer with an error of its own, naming no kind."""
43
+ return ToolResult(content="no", is_error=True)
44
+
45
+
46
+ @tool("kind_without_error")
47
+ def kind_without_error_tool() -> ToolResult:
48
+ """Answer successfully while naming a kind."""
49
+ return ToolResult(content="fine", error_kind=ToolErrorKind.EXECUTION)
50
+
51
+
40
52
  class TestToolCallAgent:
41
53
  async def test_execute_tool_call(self) -> None:
42
54
  registry = ToolRegistry([add_tool])
@@ -217,6 +229,7 @@ class TestToolCallAgent:
217
229
  assert result.result.tool_use_id == "call_2"
218
230
  assert result.result.text == "Tool execution interrupted"
219
231
  assert result.result.is_error is True
232
+ assert result.error_kind == ToolErrorKind.EXECUTION
220
233
 
221
234
  async def test_a_tool_that_raises_interrupted_is_interrupted_not_failed(
222
235
  self, caplog: pytest.LogCaptureFixture
@@ -242,3 +255,57 @@ class TestToolCallAgent:
242
255
  assert result.result.text == "Tool execution interrupted"
243
256
  assert result.result.is_error is True
244
257
  assert caplog.records == []
258
+
259
+ @pytest.mark.parametrize(
260
+ ("tool_name", "error_kind"),
261
+ [("refusing", ToolErrorKind.EXECUTION), ("kind_without_error", None)],
262
+ )
263
+ async def test_a_tool_answer_carries_a_kind_exactly_when_it_is_an_error(
264
+ self, tool_name: str, error_kind: ToolErrorKind | None
265
+ ) -> None:
266
+ runtime = AgentRuntime(
267
+ agents={"tool_call": ToolCallAgent},
268
+ services={
269
+ TurnConfig: TurnConfig(
270
+ tools=ToolRegistry([refusing_tool, kind_without_error_tool]),
271
+ llm_config=LLMConfig(provider=_UNUSED_PROVIDER, model="test"),
272
+ system=[],
273
+ history=[],
274
+ )
275
+ },
276
+ )
277
+
278
+ result = await runtime.run(
279
+ agent=ToolCallAgent, spec=ToolCallSpec(tool_use=ToolUseBlock(id="call_4", name=tool_name, input={}))
280
+ )
281
+
282
+ assert isinstance(result, ToolCallResult)
283
+ assert result.error_kind == error_kind
284
+
285
+
286
+ class TestToolCallResult:
287
+ def test_an_error_without_a_kind_gets_execution(self) -> None:
288
+ result = ToolCallResult(
289
+ tool_use=ToolUseBlock(id="c", name="t", input={}),
290
+ result=ToolResultBlock(tool_use_id="c", content=[], is_error=True),
291
+ )
292
+
293
+ assert result.error_kind == ToolErrorKind.EXECUTION
294
+
295
+ def test_an_error_keeps_the_kind_it_has(self) -> None:
296
+ result = ToolCallResult(
297
+ tool_use=ToolUseBlock(id="c", name="t", input={}),
298
+ result=ToolResultBlock(tool_use_id="c", content=[], is_error=True),
299
+ error_kind=ToolErrorKind.SCHEMA_VALIDATION,
300
+ )
301
+
302
+ assert result.error_kind == ToolErrorKind.SCHEMA_VALIDATION
303
+
304
+ def test_an_answer_that_is_not_an_error_carries_no_kind(self) -> None:
305
+ result = ToolCallResult(
306
+ tool_use=ToolUseBlock(id="c", name="t", input={}),
307
+ result=ToolResultBlock(tool_use_id="c", content=[]),
308
+ error_kind=ToolErrorKind.EXECUTION,
309
+ )
310
+
311
+ assert result.error_kind is None