2 Commits

Author SHA1 Message Date
hurui200320 306f114b8a feat(agents): add tool-call support to LLMAgent.stream_message
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
2026-07-02 07:47:39 +00:00
CoreRasurae 227e2feece feat(agents): implement multi-turn tool-call loop and sandbox improvements
CI / build (pull_request) Successful in 45s
CI / quality (pull_request) Successful in 58s
CI / lint (pull_request) Successful in 1m15s
CI / typecheck (pull_request) Successful in 1m15s
CI / security (pull_request) Successful in 1m15s
CI / integration_tests (pull_request) Successful in 1m24s
CI / unit_tests (pull_request) Successful in 3m25s
CI / coverage (pull_request) Successful in 3m10s
CI / status-check (pull_request) Successful in 6s
CI / lint (push) Successful in 42s
CI / quality (push) Successful in 48s
CI / typecheck (push) Successful in 51s
CI / security (push) Successful in 49s
CI / build (push) Successful in 47s
CI / integration_tests (push) Successful in 1m10s
CI / unit_tests (push) Successful in 3m9s
CI / coverage (push) Successful in 3m7s
CI / status-check (push) Successful in 3s
Implements the complete LLM agent tool-calling pipeline — the tool-calling
path was broken in multiple ways: tools from agent config were not passed
to the LLM, tool execution was single-pass (the model could not see tool
results or make follow-up calls), tool errors were discarded, and shell/
python_exec tools were never registered.

Key changes:

- Multi-turn tool-call loop: passes tools to the LLM via ainvoke(tools=...)
  and re-invokes after each batch of ToolResults, with a configurable
  round limit (tool_max_rounds config / TOOL_MAX_ROUNDS env var, default 20).

- Tool schema module (llm_tools.py): normalize_tool_entry() converts
  string tool names and config dicts to OpenAI function-calling format
  with full parameter schemas and LLM-facing guidance (max_chars for
  file_read, sandbox builtins for python_exec, etc.).

- file_read enhancements: directory listing with file sizes and type
  indicators, max_chars truncation to prevent context overflow, shell
  command detection with redirect to shell tool, file-not-found
  recovery hints with parent directory listing.

- python_exec sandbox: stdout capture so print() produces visible
  output, NameError guidance directing LLM to file_read/file_write
  instead of using open().

- shell/python_exec tool registration in builtin_tools when
  allow_shell / exec_python is enabled.

- create_subprocess_shell for shell commands (was create_subprocess_exec,
  which blocks pipes/redirections/chains).

- Tool error propagation: ExecutionError/ConfigurationError details
  included in ToolMessages for LLM self-correction.

- Stuck-model recovery: injects synthesizing prompt when model
  exhausts tool rounds without producing content.

- LLM provider error details preserved in ExecutionError messages
  instead of generic "LLM processing failed".

- Empty message filtering in _prepare_conversation_history() prevents
  whitespace-only messages from polluting downstream conversation
  history in multi-agent pipelines.

Closes #59
2026-06-23 12:39:20 +01:00