Files
hurui200320 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
feat(agents): add tool-call support to LLMAgent.stream_message
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
2026-07-02 07:47:39 +00:00
..