306f114b8a
CI / lint (pull_request) Successful in 47s
CI / typecheck (pull_request) Successful in 51s
CI / security (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m7s
CI / integration_tests (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 35s
CI / coverage (pull_request) Successful in 3m9s
CI / status-check (pull_request) Successful in 3s
CI / lint (push) Successful in 34s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 50s
CI / quality (push) Successful in 33s
CI / unit_tests (push) Successful in 3m6s
CI / integration_tests (push) Successful in 1m7s
CI / build (push) Successful in 33s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Extract the entire multi-turn tool-call orchestration from the inline loop inside process_message() into a new shared private method _execute_tool_loop(). Both process_message() and stream_message() now delegate to this single implementation, eliminating the previous capability gap where stream_message() had no tool-calling support. Design: - _ToolLoopResult dataclass: carries final_response, accumulated_prompt, accumulated_completion, budget_exhausted, and synthesis_was_run. Both callers read token counts from this object instead of local sentinels. - _ToolLoopError exception: wraps any exception raised inside the helper and carries partial accumulated token counts + any_invocation_made flag. Callers catch this, set their _captured_prompt/_captured_completion sentinels (billing integrity), then re-raise the original cause. - _execute_tool_loop(): extracted verbatim from process_message(). Contains the full ainvoke() loop, per-tool ToolAgent dispatch, token accumulation across rounds, token-budget exhaustion + synthesis path (§4.4.7 D-4), stuck-model synthesis prompt, and tool-output pruning pass (§4.4.8). On exception, wraps in _ToolLoopError for the caller. process_message() refactor: - Replaces ~490 lines of inline tool loop with a 15-line delegation. For tool-configured actors: awaits _execute_tool_loop(), extracts response text and token counts from the result. For no-tool actors: single plain ainvoke() (unchanged behaviour). Billing-integrity sentinels (_captured_prompt/_captured_completion) are set from _ToolLoopError on exception and from the result on success. All existing Behave scenarios continue to pass unmodified. stream_message() integration: - When tools are configured: awaits _execute_tool_loop() and yields the final response as a single content chunk. Token counts come from the result's accumulated_prompt/accumulated_completion fields (spanning all tool rounds + pruning passes). Memory update is performed before the generator returns. Billing sentinels are updated from _ToolLoopError on exception. - When no tools are configured: the existing astream() token-by-token path runs completely unchanged. Single-chunk trade-off: when tools are involved, the user already waits for tool execution before the final answer begins, so yielding that answer as one chunk vs. token-by-token has minimal UX impact. The alternative (re-calling astream() after ainvoke() produced the final response) would double LLM cost for every tool-using request. Webapp workaround: the actor_config_has_tools fallback in cleveragents/cleveragents-webapp#328 (stream_fallback_to_non_stream in _execute_via_cleveractors_stream / _generate_actor_sse) can now be removed. Tests added: - features/llm_agent_tool_loop.feature + steps: 10 Behave scenarios covering _execute_tool_loop() internals independently (single round, multi-round, max_rounds exhaustion, token accumulation, budget exhaustion, stuck-model synthesis, pruning pass, tool dispatch error, exception path with partial counts, pre-invocation ConfigurationError). - features/llm_agent_stream_tool_calls.feature + steps: 7 Behave scenarios for stream_message() with tool-call support (single chunk output, two-round token sum, final text, no-tools astream unchanged, no-tools token counts, exception billing integrity, memory update). - robot/llm_tool_calling.robot: two new Robot integration tests: 'Streaming Path Exercises Tool Loop Via Execute Stream' and 'Streaming Path Accumulates Tokens Across Two Tool Rounds'. - robot/ToolCallingTestLib.py: new keywords and create_executor_with_multi_round_tool_calling_agent factory. Quality gates: format ✓ lint ✓ typecheck ✓ security_scan ✓ unit_tests ✓ integration_tests ✓ coverage_report ✓ (97.0%) ISSUES CLOSED: #67