flowra 0.0.92.dev109__tar.gz → 0.0.92.dev110__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.
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/CHANGELOG.md +14 -19
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/PKG-INFO +1 -1
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/context7.json +1 -1
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/agents.md +4 -3
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/observability.md +18 -13
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/tools.md +4 -2
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/agent.py +30 -25
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/hook_events.py +4 -1
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/tool_call/agent.py +12 -19
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/version.py +1 -1
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_turn_agent.py +105 -7
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.agents/skills/source-command-update-pricing/SKILL.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.claude/commands/update-pricing.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.env.example +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.github/CODEOWNERS +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.github/workflows/master.yml +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.github/workflows/publish.yml +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.github/workflows/pull_request.yml +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.gitignore +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.python-version +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/AGENTS.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/CLAUDE.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/LICENSE +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/Makefile +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/README.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/architecture.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/getting-started.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/llm.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/patterns.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/agent_async_init.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/agent_tool_errors.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/anthropic_web_search.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/e2e_security.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/enum_slot_values.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/final_tools.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/langchain_comparison.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/lifecycle_hooks_and_conversation.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/llm_retry_backoff.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/named_scope_lazy_hydration.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/named_scope_type_registry.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/named_scopes.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/pricing_complexity.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/pydantic_ai_comparison.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/strands_comparison.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/tool_error_signals.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/tool_errors_vs_failures.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/tool_registry_redesign.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/tool_search_tool.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/voice_stt.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step1_structure.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step2_code_style.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step3_documentation.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step4_doc_readability.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step5_doc_audit.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/review_prompts/step6_tests.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/todo.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/video_script.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/TRACING_COMBINATIONS.md +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/agent_as_tool.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/app_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/console_chat.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/agents_custom.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/agents_parallel.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/getting_started_chat.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/getting_started_streaming.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/getting_started_tools.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/llm_streaming.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/llm_structured_output.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/docs/tools_service_injection.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/escalation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/llm_logging.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/llm_routing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/menu_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/menu_agent_class.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_dual_export_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_nested_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_otel_both_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_otel_nested_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/mlflow_parallel_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/model_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/otel_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/otel_jaeger_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/otel_nested_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/otel_visualize.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/race.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/span_crash_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/span_demo.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/system_prompt.txt +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/tools/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/tools/calculator.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/tools/random_numbers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/tools/switch_model.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/examples/tui_chat.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/_sentinel.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/_typing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/_path.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/actions.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/agent_arg.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/compiler.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/contract.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/init_params.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/instance.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/step_params.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/step_validation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/steps.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/compile/type_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/model.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/shared_persistent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/spawn.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/step.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/definition/step_helpers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/context.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/flowing_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/hooks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/interrupt.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/interrupt_helpers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/flow/timeout.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/engine.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/execution.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/instance_factory.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/key_locks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/runtime.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/scope.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/serialization.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/spans.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/spawn_satisfaction.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/spawn_tree.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/runtime/visibility.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/services.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/session_state.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/state/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/state/markers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/state/persistent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/state/store.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/state/values.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/storage/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/storage/file.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/storage/in_memory.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/agent/storage/session_storage.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/ext/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/ext/_session.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/ext/mlflow.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/ext/otel.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/config.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/conversation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/hook_events.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/chat/spec.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/config_value.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_call/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_call/agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_call/config.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_call/spec.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_caller.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/llm_config.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/observability/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/observability/llm_hooks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/config.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/context.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/spec.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/tool_call/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/tool_call/agent_tool.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/lib/turn/tool_call/context.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/_schema_nodes.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/base.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/blocks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/errors.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/anthropic/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/anthropic/cache.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/anthropic/presets.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/anthropic/tool_search.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/anthropic/web_search.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/openai/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/ext/openai/tool_search.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/gemini_schema.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/messages.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/openai_schema.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/bundled.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/custom/anthropic.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/custom/vertex.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/anthropic.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/azure_openai.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/bedrock.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/gemini_api.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/inception.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/openai.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/data/generated/vertex.json +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/model_pricing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/pricing/price_list.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/provider.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/_openai_common.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/anthropic.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/anthropic_bedrock.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/anthropic_foundry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/anthropic_vertex.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/azure_openai.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/google_vertex.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/openai.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/providers/openai_responses.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/request.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/response.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/schema_formatting.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/schema_validation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/stream.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/llm/tools.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/py.typed +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/_mcp_content.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/_mcp_headers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/local_tool.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/mcp_connection.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/result_format.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/tool_arg.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/tool_filter.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/tool_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/flowra/tools/types.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/pyproject.toml +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/compile/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/compile/test_compile.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/test_actions.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/test_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/test_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/test_spawn.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/definition/test_step_helpers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_context.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_flowing_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_flowing_registry_tasks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_flowing_sync.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_hooks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_interrupt.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_interruptible.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_spans.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_timeout.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/flow/test_with_interrupt.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_engine.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_engine_spans.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_ephemeral_isolation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_hook_context.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_named_scope_persistence.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_named_scope_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_named_scopes.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_parallel_failure.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_persistence.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_primitive_join.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_runtime.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_scope.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_serialization.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/runtime/test_spec_in_constructor.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/state/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/state/test_values.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/storage/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/storage/test_file.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/storage/test_in_memory.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/test_missing_scenarios.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/agent/test_session_state.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/examples/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/examples/test_menu_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/ext/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/ext/_mlflow.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/ext/test_mlflow.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/ext/test_mlflow_cross_task.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/ext/test_otel.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/_helpers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/ext/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/ext/anthropic/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/ext/anthropic/test_anthropic.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/ext/openai/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/ext/openai/test_openai_tool_search.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_chat_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_chat_spec.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_config_value.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_conversation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_llm_call_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_llm_caller.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_matches_tool_filter.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_tool_call_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/test_tool_call_agent_call_agent.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/turn/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/turn/test_per_call_headers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/turn/test_render.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/lib/turn/test_tool_errors.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/pricing/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/pricing/test_bundled.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/pricing/test_model_pricing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/pricing/test_price_list.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/_mock_http.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/_raw_body.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/_tool_result_blocks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_bedrock.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_bedrock_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_foundry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_foundry_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_vertex.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_anthropic_vertex_locations_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_azure_openai_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_cross_provider_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_errors.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_fallback_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_google_vertex.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_google_vertex_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_history_across_models.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_openai_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_openai_provider.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_openai_responses.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_openai_responses_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_refusals.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/providers/test_schemas_e2e.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_blocks.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_cost_breakdown.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_gemini_schema.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_metadata.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_openai_schema.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_response.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_schema_formatting.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_schema_nodes.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_schema_validation.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/llm/test_stream.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/scripts/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/scripts/test_sync_pricing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/test_typing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/__init__.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_local_tool.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_mcp_connection.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_mcp_headers.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_result_format.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_tool_registry.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tests/tools/test_types.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/tools/sync_pricing.py +0 -0
- {flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/uv.lock +0 -0
|
@@ -9,29 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
- **`BeforeToolCallsEvent`: answer a model message's tool calls before any
|
|
12
|
-
runs.**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
12
|
+
runs.** It fires once per model message that has tool calls, even when it
|
|
13
|
+
has only one, and lists them in `tool_uses`. A handler puts a `ToolResult`
|
|
14
|
+
into `results` under a call's id: that call does not run, and the model gets
|
|
15
|
+
the handler's answer. Use it for a rule about the message as a whole, such as
|
|
16
|
+
a tool that must be called alone. Details in
|
|
17
|
+
[Pre-built events](docs/observability.md#pre-built-events).
|
|
24
18
|
- **`AfterToolCallEvent.prefilled`**: true for a call answered in
|
|
25
19
|
`BeforeToolCallsEvent`, so a handler can tell that the tool did not run.
|
|
26
20
|
|
|
27
21
|
### Changed
|
|
28
|
-
- **A call's answer carries an `error_kind` exactly when it is an error.**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
- **A call's answer carries an `error_kind` exactly when it is an error.** An
|
|
23
|
+
error that comes without a kind reaches `AfterToolCallEvent` and
|
|
24
|
+
`ToolCallSpan` as `ToolErrorKind.EXECUTION`, and an answer that is not an
|
|
25
|
+
error carries no kind. A consumer that tells an error by its kind now sees
|
|
26
|
+
every error. Details in [Error handling](docs/tools.md#errors-the-tool-chooses).
|
|
27
|
+
- **A `BeforeToolCallEvent` handler that changes a call's `id` raises
|
|
28
|
+
`ValueError`.** The id ties a call to its result, so a replaced `tool_use`
|
|
29
|
+
keeps it.
|
|
35
30
|
|
|
36
31
|
## [0.0.91] - 2026-10-20
|
|
37
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: flowra
|
|
3
|
-
Version: 0.0.92.
|
|
3
|
+
Version: 0.0.92.dev110
|
|
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
|
|
@@ -134,7 +134,7 @@
|
|
|
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, BeforeToolCallsEvent (once per model message
|
|
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 that has tool calls, even when it has only one, 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; an answered call is not looked up in the registry, so an answer to an unknown or denied tool reaches the model as given, not as Unknown tool; a key not among the message's call ids raises ValueError), BeforeToolCallEvent (has tool_definition: ToolDefinition | None; mutable tool_use (a replacement keeps the call's id; changing it raises ValueError), 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)",
|
|
@@ -808,9 +808,10 @@ Lifecycle events (from `flowra.lib.turn`):
|
|
|
808
808
|
| `ResultMessageEvent` | LLM finished (END_TURN) | `continue_messages` |
|
|
809
809
|
|
|
810
810
|
A tool call cut off by the output limit appears in `BeforeToolCallsEvent` but
|
|
811
|
-
fires no `BeforeToolCallEvent` or `AfterToolCallEvent
|
|
812
|
-
`BeforeToolCallsEvent` does not run;
|
|
813
|
-
[Pre-built events](observability.md#pre-built-events)
|
|
811
|
+
fires no `BeforeToolCallEvent` or `AfterToolCallEvent`. A call a handler answers
|
|
812
|
+
in `BeforeToolCallsEvent.results` does not run; it still fires
|
|
813
|
+
`AfterToolCallEvent`. See [Pre-built events](observability.md#pre-built-events)
|
|
814
|
+
in Observability.
|
|
814
815
|
|
|
815
816
|
Streaming events (from `flowra.lib.observability`):
|
|
816
817
|
|
|
@@ -69,21 +69,18 @@ def add_hint(event: Event[AfterToolCallEvent]) -> None:
|
|
|
69
69
|
runtime.hooks.on(AfterToolCallEvent, add_hint)
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
`
|
|
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
|
|
72
|
+
`BeforeToolCallsEvent` fires once per model message that has tool calls, even
|
|
73
|
+
when it has only one, before any of them runs. A handler answers a call itself
|
|
74
|
+
by putting a `ToolResult` into `results` under the call's id: that call does not
|
|
81
75
|
run, and the others run as usual. An answered call gets no `BeforeToolCallEvent`
|
|
82
76
|
and no `ToolCallSpan`; its result goes through `AfterToolCallEvent` (with
|
|
83
77
|
`prefilled=True`) and `max_consecutive_errors` like any other, in the message's
|
|
84
78
|
order. An error answered without an `error_kind` arrives with
|
|
85
|
-
`ToolErrorKind.EXECUTION`, as any error a tool gives without one does.
|
|
86
|
-
|
|
79
|
+
`ToolErrorKind.EXECUTION`, as any error a tool gives without one does. An
|
|
80
|
+
answered call is not looked up in the registry, so a handler can answer a call
|
|
81
|
+
to a tool that is unknown or denied: the model gets the handler's answer, not
|
|
82
|
+
`Unknown tool`. A key that is not the id of a call in the message raises
|
|
83
|
+
`ValueError`.
|
|
87
84
|
|
|
88
85
|
```python
|
|
89
86
|
from flowra.agent import Event
|
|
@@ -104,6 +101,12 @@ def reply_alone(event: Event[BeforeToolCallsEvent]) -> None:
|
|
|
104
101
|
runtime.hooks.on(BeforeToolCallsEvent, reply_alone)
|
|
105
102
|
```
|
|
106
103
|
|
|
104
|
+
A tool call cut off by the output limit (see [TurnAgent](agents.md#turnagent)) is
|
|
105
|
+
never executed. It gets an error result directly, with no `BeforeToolCallEvent`,
|
|
106
|
+
`AfterToolCallEvent` or `ToolCallSpan`, so hooks that audit or rewrite tool
|
|
107
|
+
results don't see it. It does appear in `BeforeToolCallsEvent.tool_uses`, but an
|
|
108
|
+
answer put under its id is not used.
|
|
109
|
+
|
|
107
110
|
A tool call that fails the run (an exception raised around a tool's executor,
|
|
108
111
|
see [Error handling](tools.md#what-fails-the-run)) has no result:
|
|
109
112
|
`AfterToolCallEvent` doesn't fire, and its `ToolCallSpan` closes with `error`
|
|
@@ -137,8 +140,10 @@ runtime = AgentRuntime(agents={...}, services={...})
|
|
|
137
140
|
runtime.hooks.on(BeforeToolCallEvent, on_before_tool)
|
|
138
141
|
```
|
|
139
142
|
|
|
140
|
-
A `BeforeToolCallEvent` handler can
|
|
141
|
-
|
|
143
|
+
A `BeforeToolCallEvent` handler can replace `tool_use` to change a call's input,
|
|
144
|
+
but the new block keeps the call's `id`: changing the id raises `ValueError`.
|
|
145
|
+
It can also fill `headers` and `meta` to pass per-call data to the
|
|
146
|
+
tool — see [ToolCallInfo](tools.md#toolcallinfo).
|
|
142
147
|
|
|
143
148
|
Use `event.context.find_spec(SpecType)` to walk the execution stack and find
|
|
144
149
|
the spec of a parent agent — useful for identifying which model or session is
|
|
@@ -685,8 +685,10 @@ async def respond(params: RespondParams) -> Sent:
|
|
|
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
687
|
gives the same answer. Without `error_kind`, an error still reaches hooks and
|
|
688
|
-
spans as `EXECUTION
|
|
689
|
-
|
|
688
|
+
spans as `EXECUTION`. What they receive (`ToolCallResult`, `AfterToolCallEvent`,
|
|
689
|
+
`ToolCallSpan`) carries a kind exactly when it is an error, so there
|
|
690
|
+
`error_kind is not None` and `is_error` never disagree. The `ToolResult` that
|
|
691
|
+
`registry.execute` returns keeps the kind the tool gave it.
|
|
690
692
|
|
|
691
693
|
`ToolError` is for `@tool` functions: only their executor answers it. An agent
|
|
692
694
|
behind an [agent tool](#agents-as-tools) returns its error as a `ToolResult`
|
|
@@ -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
|
|
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
|
|
@@ -64,10 +64,18 @@ __all__ = ["TurnAgent"]
|
|
|
64
64
|
logger = logging.getLogger(__name__)
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
@dataclasses.dataclass(frozen=True, slots=True, kw_only=True)
|
|
68
|
+
class _AnsweredCalls:
|
|
69
|
+
"""The calls a ``BeforeToolCallsEvent`` handler answered, with the ids of the message's calls that can run,
|
|
70
|
+
in the message's order."""
|
|
71
|
+
|
|
72
|
+
results: list[ToolCallResult]
|
|
73
|
+
call_order: list[str]
|
|
74
|
+
|
|
75
|
+
|
|
67
76
|
class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
68
77
|
__slots__ = (
|
|
69
|
-
"
|
|
70
|
-
"__call_order",
|
|
78
|
+
"__answered_calls",
|
|
71
79
|
"__caller",
|
|
72
80
|
"__config",
|
|
73
81
|
"__consecutive_errors",
|
|
@@ -100,8 +108,7 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
100
108
|
self.__usage = store.ephemeral.slot(Scalar[Usage | None], "usage")
|
|
101
109
|
self.__consecutive_errors = store.ephemeral.slot(Scalar[dict[str, int]], "consecutive_errors")
|
|
102
110
|
self.__incomplete_tool_result = store.ephemeral.slot(Scalar[ToolResultBlock | None], "incomplete_tool_result")
|
|
103
|
-
self.
|
|
104
|
-
self.__call_order = store.ephemeral.slot(Scalar[list[str]], "call_order")
|
|
111
|
+
self.__answered_calls = store.ephemeral.slot(Scalar[_AnsweredCalls | None], "answered_calls")
|
|
105
112
|
self.__interrupt = interrupt
|
|
106
113
|
self.__context = TurnAgentContext()
|
|
107
114
|
self.__config = config
|
|
@@ -296,9 +303,9 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
296
303
|
consecutive_errors = dict(self.__consecutive_errors.get({}))
|
|
297
304
|
max_consecutive = await resolve_config_value(self.__config.max_consecutive_errors)
|
|
298
305
|
|
|
299
|
-
answered = self.
|
|
300
|
-
answered_ids = {tc_result.tool_use.id for tc_result in answered}
|
|
301
|
-
collected = self.__in_call_order(
|
|
306
|
+
answered = self.__answered_calls.get(None)
|
|
307
|
+
answered_ids = {tc_result.tool_use.id for tc_result in answered.results} if answered is not None else set()
|
|
308
|
+
collected = self.__in_call_order(answered, results) if answered is not None else results
|
|
302
309
|
for tc_result in collected:
|
|
303
310
|
after_event = await self.__hooks.emit(
|
|
304
311
|
AfterToolCallEvent(
|
|
@@ -343,9 +350,8 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
343
350
|
if incomplete_result is not None:
|
|
344
351
|
result_blocks.append(incomplete_result)
|
|
345
352
|
self.__incomplete_tool_result.set(None)
|
|
346
|
-
if answered:
|
|
347
|
-
self.
|
|
348
|
-
self.__call_order.set([])
|
|
353
|
+
if answered is not None:
|
|
354
|
+
self.__answered_calls.set(None)
|
|
349
355
|
|
|
350
356
|
self.__consecutive_errors.set(consecutive_errors)
|
|
351
357
|
user_message = UserMessage(blocks=list(result_blocks))
|
|
@@ -398,13 +404,14 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
398
404
|
raise ValueError(f"No tool call {tool_use_id!r} in this message")
|
|
399
405
|
# Answers are taken from the calls that can run, so the cut-off call keeps its own error result
|
|
400
406
|
answered = [
|
|
401
|
-
|
|
407
|
+
ToolCallResult.from_tool_result(tool_use, calls_event.results[tool_use.id])
|
|
402
408
|
for tool_use in tool_uses
|
|
403
409
|
if tool_use.id in calls_event.results
|
|
404
410
|
]
|
|
405
411
|
if answered:
|
|
406
|
-
self.
|
|
407
|
-
|
|
412
|
+
self.__answered_calls.set(
|
|
413
|
+
_AnsweredCalls(results=answered, call_order=[tool_use.id for tool_use in tool_uses])
|
|
414
|
+
)
|
|
408
415
|
|
|
409
416
|
amendments: dict[str, ToolUseBlock] = {}
|
|
410
417
|
final_calls: list[BeforeToolCallEvent] = []
|
|
@@ -418,6 +425,11 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
418
425
|
context=self.__context,
|
|
419
426
|
)
|
|
420
427
|
)
|
|
428
|
+
if before_event.tool_use.id != tool_use.id:
|
|
429
|
+
raise ValueError(
|
|
430
|
+
f"A BeforeToolCallEvent handler changed the id of tool call {tool_use.id!r} to "
|
|
431
|
+
f"{before_event.tool_use.id!r}; a call's id must stay as the model gave it"
|
|
432
|
+
)
|
|
421
433
|
final_calls.append(before_event)
|
|
422
434
|
if before_event.tool_use is not tool_use:
|
|
423
435
|
amendments[tool_use.id] = before_event.tool_use
|
|
@@ -447,19 +459,12 @@ class TurnAgent(Agent[TurnSpec, TurnResult]):
|
|
|
447
459
|
]
|
|
448
460
|
return Spawn(children=children, then=self.collect_results)
|
|
449
461
|
|
|
450
|
-
|
|
462
|
+
@staticmethod
|
|
463
|
+
def __in_call_order(answered: _AnsweredCalls, results: list[ToolCallResult]) -> list[ToolCallResult]:
|
|
451
464
|
# By the order the message's calls had when they were made, kept beside the answers: it holds
|
|
452
465
|
# 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(
|
|
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
|
-
)
|
|
466
|
+
position = {tool_use_id: i for i, tool_use_id in enumerate(answered.call_order)}
|
|
467
|
+
return sorted([*answered.results, *results], key=lambda tc_result: position[tc_result.tool_use.id])
|
|
463
468
|
|
|
464
469
|
@staticmethod
|
|
465
470
|
def __incomplete_tool_use_message(stop_reason: StopReason) -> str:
|
|
@@ -105,6 +105,8 @@ class BeforeToolCallsEvent:
|
|
|
105
105
|
An answer under the cut-off call's id is not used: that call always gets its own error result.
|
|
106
106
|
Put a ``ToolResult`` into ``results`` under a call's id to answer it without running it: the
|
|
107
107
|
other calls run as usual, and the result reaches the model as that call's answer.
|
|
108
|
+
An answered call is not looked up in the registry, so a handler can answer a call to a tool that
|
|
109
|
+
is unknown or denied: the model gets the handler's answer, not ``Unknown tool``.
|
|
108
110
|
"""
|
|
109
111
|
|
|
110
112
|
tool_uses: list[ToolUseBlock]
|
|
@@ -117,7 +119,8 @@ class BeforeToolCallEvent:
|
|
|
117
119
|
"""Emitted before each tool execution.
|
|
118
120
|
|
|
119
121
|
Replace ``tool_use`` to modify tool parameters (e.g. sanitize input,
|
|
120
|
-
inject defaults).
|
|
122
|
+
inject defaults). The replacement keeps the call's ``id``: changing it
|
|
123
|
+
raises ``ValueError``.
|
|
121
124
|
|
|
122
125
|
Fill ``headers`` and ``meta`` to pass per-call data the model never sees:
|
|
123
126
|
MCP tools send them as HTTP headers and ``params._meta`` of the
|
|
@@ -58,6 +58,14 @@ class ToolCallResult:
|
|
|
58
58
|
error_kind = (self.error_kind or ToolErrorKind.EXECUTION) if self.result.is_error else None
|
|
59
59
|
object.__setattr__(self, "error_kind", error_kind)
|
|
60
60
|
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_tool_result(cls, tool_use: ToolUseBlock, result: ToolResult) -> "ToolCallResult":
|
|
63
|
+
return cls(
|
|
64
|
+
tool_use=tool_use,
|
|
65
|
+
result=ToolResultBlock(tool_use_id=tool_use.id, content=result.blocks, is_error=result.is_error),
|
|
66
|
+
error_kind=result.error_kind,
|
|
67
|
+
)
|
|
68
|
+
|
|
61
69
|
|
|
62
70
|
class ToolCallAgent(Agent[ToolCallSpec, ToolCallResult]):
|
|
63
71
|
"""Executes a single tool call and returns the result.
|
|
@@ -150,12 +158,7 @@ class ToolCallAgent(Agent[ToolCallSpec, ToolCallResult]):
|
|
|
150
158
|
then=self.collect_agent_result,
|
|
151
159
|
)
|
|
152
160
|
|
|
153
|
-
|
|
154
|
-
tool_use_id=tool_use.id,
|
|
155
|
-
content=execution_result.blocks,
|
|
156
|
-
is_error=execution_result.is_error,
|
|
157
|
-
)
|
|
158
|
-
return ToolCallResult(tool_use=tool_use, result=result, error_kind=execution_result.error_kind)
|
|
161
|
+
return ToolCallResult.from_tool_result(tool_use, execution_result)
|
|
159
162
|
|
|
160
163
|
@step("collect_agent_result")
|
|
161
164
|
async def collect_agent_result(self, agent_result: Annotated[Any, agent_arg.CHILD]) -> ToolCallResult:
|
|
@@ -163,19 +166,9 @@ class ToolCallAgent(Agent[ToolCallSpec, ToolCallResult]):
|
|
|
163
166
|
if isinstance(agent_result, Interrupted):
|
|
164
167
|
return self.__error_result(tool_use, "Tool execution interrupted")
|
|
165
168
|
if isinstance(agent_result, ToolResult):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
is_error=agent_result.is_error,
|
|
170
|
-
)
|
|
171
|
-
error_kind = agent_result.error_kind
|
|
172
|
-
else:
|
|
173
|
-
result = ToolResultBlock(
|
|
174
|
-
tool_use_id=tool_use.id,
|
|
175
|
-
content=[TextBlock(text=format_result(agent_result))],
|
|
176
|
-
)
|
|
177
|
-
error_kind = None
|
|
178
|
-
return ToolCallResult(tool_use=tool_use, result=result, error_kind=error_kind)
|
|
169
|
+
return ToolCallResult.from_tool_result(tool_use, agent_result)
|
|
170
|
+
result = ToolResultBlock(tool_use_id=tool_use.id, content=[TextBlock(text=format_result(agent_result))])
|
|
171
|
+
return ToolCallResult(tool_use=tool_use, result=result)
|
|
179
172
|
|
|
180
173
|
@staticmethod
|
|
181
174
|
def __error_result(tool_use: ToolUseBlock, content: str, error_kind: ToolErrorKind | None = None) -> ToolCallResult:
|
|
@@ -19,7 +19,7 @@ from flowra.agent import (
|
|
|
19
19
|
)
|
|
20
20
|
from flowra.agent.flow.hooks import HooksImpl
|
|
21
21
|
from flowra.lib import LLMConfig
|
|
22
|
-
from flowra.lib.observability import LLMCallSpan, TextDeltaEvent, ThinkingDeltaEvent, UsageEvent
|
|
22
|
+
from flowra.lib.observability import LLMCallSpan, TextDeltaEvent, ThinkingDeltaEvent, ToolCallSpan, UsageEvent
|
|
23
23
|
from flowra.lib.turn import (
|
|
24
24
|
AfterToolCallEvent,
|
|
25
25
|
BeforeToolCallEvent,
|
|
@@ -2466,8 +2466,8 @@ def _calls_a_and_b() -> ScriptedProvider:
|
|
|
2466
2466
|
)
|
|
2467
2467
|
|
|
2468
2468
|
|
|
2469
|
-
def _cut_off_after_a(
|
|
2470
|
-
return
|
|
2469
|
+
def _cut_off_after_a() -> ScriptedProvider:
|
|
2470
|
+
return ScriptedProvider(
|
|
2471
2471
|
LLMResponse(
|
|
2472
2472
|
messages=[
|
|
2473
2473
|
AssistantMessage(
|
|
@@ -2527,6 +2527,25 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2527
2527
|
assert before == ["b1"]
|
|
2528
2528
|
assert after == {"a1": True, "b1": False}
|
|
2529
2529
|
|
|
2530
|
+
async def test_answered_call_gets_no_tool_call_span(self) -> None:
|
|
2531
|
+
spans: list[str] = []
|
|
2532
|
+
|
|
2533
|
+
def record_span(event: Event[ToolCallSpan]) -> SpanExitHandler:
|
|
2534
|
+
spans.append(event.data.tool_use.id)
|
|
2535
|
+
|
|
2536
|
+
def on_exit(error: BaseException | None = None) -> None:
|
|
2537
|
+
pass
|
|
2538
|
+
|
|
2539
|
+
return on_exit
|
|
2540
|
+
|
|
2541
|
+
hooks = _answer_by_name({"a": ToolResult(content="no", is_error=True)})
|
|
2542
|
+
hooks.on_span(ToolCallSpan, record_span)
|
|
2543
|
+
registry = ToolRegistry([_recording_tool("a", []), _recording_tool("b", [])])
|
|
2544
|
+
|
|
2545
|
+
await _run_turn(_calls_a_and_b(), registry, hooks=hooks)
|
|
2546
|
+
|
|
2547
|
+
assert spans == ["b1"]
|
|
2548
|
+
|
|
2530
2549
|
async def test_answered_error_without_kind_gets_execution(self) -> None:
|
|
2531
2550
|
kinds: dict[str, ToolErrorKind | None] = {}
|
|
2532
2551
|
hooks = _answer_by_name({"a": ToolResult(content="no", is_error=True)})
|
|
@@ -2601,6 +2620,15 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2601
2620
|
seen = _result_blocks(provider.requests[1].messages[-1])
|
|
2602
2621
|
assert [(b.tool_use_id, b.text) for b in seen] == [("a1", "no a"), ("b1", "no b")]
|
|
2603
2622
|
|
|
2623
|
+
async def test_answer_to_a_tool_not_in_the_registry_reaches_the_model(self) -> None:
|
|
2624
|
+
hooks = _answer_by_name({"a": ToolResult(content="handled")})
|
|
2625
|
+
provider = _calls_a_and_b()
|
|
2626
|
+
|
|
2627
|
+
await _run_turn(provider, ToolRegistry([_recording_tool("b", [])]), hooks=hooks)
|
|
2628
|
+
|
|
2629
|
+
a_seen, _ = _result_blocks(provider.requests[1].messages[-1])
|
|
2630
|
+
assert (a_seen.tool_use_id, a_seen.text, a_seen.is_error) == ("a1", "handled", False)
|
|
2631
|
+
|
|
2604
2632
|
async def test_answered_call_cannot_finish_the_turn(self) -> None:
|
|
2605
2633
|
hooks = _answer_by_name({"finish": ToolResult(content="not now", is_error=True)})
|
|
2606
2634
|
provider = ScriptedProvider(
|
|
@@ -2666,6 +2694,17 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2666
2694
|
assert (a_result.tool_use_id, a_result.text, a_result.is_error) == ("a1", "no", True)
|
|
2667
2695
|
assert (b_result.tool_use_id, b_result.text, b_result.is_error) == ("b1", "OK", False)
|
|
2668
2696
|
|
|
2697
|
+
async def test_before_tool_call_changing_a_calls_id_raises(self) -> None:
|
|
2698
|
+
def change_id(event: Event[BeforeToolCallEvent]) -> None:
|
|
2699
|
+
event.data.tool_use = dataclasses.replace(event.data.tool_use, id="other")
|
|
2700
|
+
|
|
2701
|
+
hooks = HooksImpl()
|
|
2702
|
+
hooks.on(BeforeToolCallEvent, change_id)
|
|
2703
|
+
registry = ToolRegistry([_recording_tool("a", []), _recording_tool("b", [])])
|
|
2704
|
+
|
|
2705
|
+
with pytest.raises(ValueError, match="changed the id of tool call 'a1' to 'other'"):
|
|
2706
|
+
await _run_turn(_calls_a_and_b(), registry, hooks=hooks)
|
|
2707
|
+
|
|
2669
2708
|
async def test_answered_error_counts_toward_max_consecutive_errors(self) -> None:
|
|
2670
2709
|
hooks = _answer_by_name({"a": ToolResult(content="no", is_error=True)})
|
|
2671
2710
|
provider = ScriptedProvider(
|
|
@@ -2687,6 +2726,54 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2687
2726
|
assert second.text == "Tool a has failed 2 times consecutively. Please try a different approach."
|
|
2688
2727
|
assert second.is_error is True
|
|
2689
2728
|
|
|
2729
|
+
async def test_answered_non_error_resets_max_consecutive_errors(self) -> None:
|
|
2730
|
+
@tool("a")
|
|
2731
|
+
def failing_tool() -> ToolResult:
|
|
2732
|
+
"""Always fail."""
|
|
2733
|
+
return ToolResult(content="a failed", is_error=True)
|
|
2734
|
+
|
|
2735
|
+
def answer_a2(event: Event[BeforeToolCallsEvent]) -> None:
|
|
2736
|
+
if event.data.tool_uses[0].id == "a2":
|
|
2737
|
+
event.data.results["a2"] = ToolResult(content="cached")
|
|
2738
|
+
|
|
2739
|
+
hooks = HooksImpl()
|
|
2740
|
+
hooks.on(BeforeToolCallsEvent, answer_a2)
|
|
2741
|
+
provider = ScriptedProvider(
|
|
2742
|
+
*(
|
|
2743
|
+
LLMResponse(
|
|
2744
|
+
messages=[AssistantMessage(blocks=[ToolUseBlock(id=call_id, name="a", input={})])],
|
|
2745
|
+
stop_reason=StopReason.TOOL_USE,
|
|
2746
|
+
)
|
|
2747
|
+
for call_id in ("a1", "a2", "a3")
|
|
2748
|
+
),
|
|
2749
|
+
LLMResponse(messages=[AssistantMessage(blocks=[TextBlock(text="Done")])], stop_reason=StopReason.END_TURN),
|
|
2750
|
+
)
|
|
2751
|
+
|
|
2752
|
+
result = await _run_turn(provider, ToolRegistry([failing_tool]), hooks=hooks, max_consecutive_errors=2)
|
|
2753
|
+
|
|
2754
|
+
[third] = _result_blocks(result.messages[6])
|
|
2755
|
+
assert (third.tool_use_id, third.text, third.is_error) == ("a3", "a failed", True)
|
|
2756
|
+
|
|
2757
|
+
async def test_round_answering_nothing_after_one_that_answered_holds_only_its_own_call(self) -> None:
|
|
2758
|
+
hooks = _answer_by_name({"a": ToolResult(content="no", is_error=True)})
|
|
2759
|
+
provider = ScriptedProvider(
|
|
2760
|
+
LLMResponse(
|
|
2761
|
+
messages=[AssistantMessage(blocks=[ToolUseBlock(id="a1", name="a", input={})])],
|
|
2762
|
+
stop_reason=StopReason.TOOL_USE,
|
|
2763
|
+
),
|
|
2764
|
+
LLMResponse(
|
|
2765
|
+
messages=[AssistantMessage(blocks=[ToolUseBlock(id="b1", name="b", input={})])],
|
|
2766
|
+
stop_reason=StopReason.TOOL_USE,
|
|
2767
|
+
),
|
|
2768
|
+
LLMResponse(messages=[AssistantMessage(blocks=[TextBlock(text="Done")])], stop_reason=StopReason.END_TURN),
|
|
2769
|
+
)
|
|
2770
|
+
registry = ToolRegistry([_recording_tool("a", []), _recording_tool("b", [])])
|
|
2771
|
+
|
|
2772
|
+
result = await _run_turn(provider, registry, hooks=hooks)
|
|
2773
|
+
|
|
2774
|
+
assert result.status == TurnStatus.COMPLETED
|
|
2775
|
+
assert [(b.tool_use_id, b.text) for b in _result_blocks(result.messages[4])] == [("b1", "b ran")]
|
|
2776
|
+
|
|
2690
2777
|
async def test_single_call_message_emits_the_event(self) -> None:
|
|
2691
2778
|
executed: list[str] = []
|
|
2692
2779
|
seen: list[list[str]] = []
|
|
@@ -2709,7 +2796,7 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2709
2796
|
async def test_before_tool_calls_sees_the_cut_off_call(self) -> None:
|
|
2710
2797
|
seen: list[list[str]] = []
|
|
2711
2798
|
hooks = _answer_by_name({}, seen)
|
|
2712
|
-
provider = _cut_off_after_a(
|
|
2799
|
+
provider = _cut_off_after_a()
|
|
2713
2800
|
|
|
2714
2801
|
await _run_turn(provider, ToolRegistry([_recording_tool("a", [])]), hooks=hooks)
|
|
2715
2802
|
|
|
@@ -2736,9 +2823,7 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2736
2823
|
hooks = _answer_by_name({"b": ToolResult(content="mine")})
|
|
2737
2824
|
hooks.on(AfterToolCallEvent, lambda event: after.append(event.data.tool_use.id))
|
|
2738
2825
|
|
|
2739
|
-
result = await _run_turn(
|
|
2740
|
-
_cut_off_after_a(ScriptedProvider), ToolRegistry([_recording_tool("a", executed)]), hooks=hooks
|
|
2741
|
-
)
|
|
2826
|
+
result = await _run_turn(_cut_off_after_a(), ToolRegistry([_recording_tool("a", executed)]), hooks=hooks)
|
|
2742
2827
|
|
|
2743
2828
|
a_result, b_result = _result_blocks(result.messages[2])
|
|
2744
2829
|
assert (a_result.tool_use_id, a_result.text) == ("a1", "a ran")
|
|
@@ -2747,6 +2832,19 @@ class TestTurnAgentBeforeToolCalls:
|
|
|
2747
2832
|
assert "output token limit" in b_result.text
|
|
2748
2833
|
assert after == ["a1"]
|
|
2749
2834
|
|
|
2835
|
+
async def test_answer_to_a_complete_call_beside_a_cut_off_one(self) -> None:
|
|
2836
|
+
executed: list[str] = []
|
|
2837
|
+
hooks = _answer_by_name({"a": ToolResult(content="mine")})
|
|
2838
|
+
registry = ToolRegistry([_recording_tool("a", executed), _recording_tool("b", executed)])
|
|
2839
|
+
|
|
2840
|
+
result = await _run_turn(_cut_off_after_a(), registry, hooks=hooks)
|
|
2841
|
+
|
|
2842
|
+
assert executed == []
|
|
2843
|
+
a_result, b_result = _result_blocks(result.messages[2])
|
|
2844
|
+
assert (a_result.tool_use_id, a_result.text, a_result.is_error) == ("a1", "mine", False)
|
|
2845
|
+
assert (b_result.tool_use_id, b_result.is_error) == ("b1", True)
|
|
2846
|
+
assert "output token limit" in b_result.text
|
|
2847
|
+
|
|
2750
2848
|
async def test_answered_call_survives_a_resume_before_results_are_collected(self) -> None:
|
|
2751
2849
|
class CrashError(Exception):
|
|
2752
2850
|
pass
|
{flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/.agents/skills/source-command-update-pricing/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flowra-0.0.92.dev109 → flowra-0.0.92.dev110}/docs/research/lifecycle_hooks_and_conversation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|