Files
cleveragents-core/features/actor_run_tool_calling.feature
T
hurui200320 91691750ed
CI / push-validation (pull_request) Successful in 35s
CI / helm (pull_request) Successful in 42s
CI / build (pull_request) Successful in 47s
CI / lint (pull_request) Successful in 1m10s
CI / typecheck (pull_request) Successful in 1m11s
CI / quality (pull_request) Successful in 1m10s
CI / security (pull_request) Successful in 1m37s
CI / integration_tests (pull_request) Failing after 4m9s
CI / unit_tests (pull_request) Successful in 4m59s
CI / docker (pull_request) Successful in 1m25s
CI / coverage (pull_request) Successful in 11m7s
CI / status-check (pull_request) Failing after 3s
fix(reactive): forward actor options block in ToolCallingLLMCaller._resolve_llm
Port the options-block merging logic from SimpleLLMAgent._resolve_llm()
(stream_router.py, added in PR #11225 / commit b3851693) to the parallel
resolution path ToolCallingLLMCaller._resolve_llm() in tool_caller.py.

Without this fix, running an actor with --skill would ignore the options
block in the actor YAML, causing openai_api_base and openai_api_key to be
silently dropped.  The provider registry then fell back to OPENAI_API_KEY
from the environment, routing requests to api.openai.com instead of the
configured local llama-swap/llama.cpp backend (HTTP 401).

Changes:
- tool_caller.ToolCallingLLMCaller._resolve_llm: read actor_config.options;
  extract openai_api_key -> __api_key_sentinel; forward allowed keys
  (openai_api_base, timeout, top_p, frequency_penalty, presence_penalty);
  reject reserved keys (provider_type, model_id) and unknown keys with
  logger.warning, matching SimpleLLMAgent behaviour exactly.
- features/actor_run_tool_calling.feature: add five BDD regression
  scenarios (section V, tagged @tdd_issue @tdd_issue_11243) covering
  api_base+key forwarding, allowed extra keys, reserved key rejection,
  unknown key rejection, and top-level precedence over options.
- features/steps/actor_run_tool_calling_steps.py: add corresponding step
  definitions for all five scenarios.

All nox quality gates pass (lint, typecheck, unit_tests 15822/0 fail,
integration_tests 1999/0 fail, coverage_report 97%).

ISSUES CLOSED: #11243
2026-05-27 12:14:04 -04:00

308 lines
15 KiB
Gherkin

@coverage @coverage_actor_run_tool_calling
Feature: Actor run tool-calling via ToolCallingRuntime
When a skill is attached to `agents actor run` via `--skill`, the actor
should perform real LLM tool calls through ToolCallingRuntime.
Background:
Given a minimal ToolCallingAgent fixture
# ---------- A: single tool call succeeds ----------
Scenario: Tool call succeeds — mock LLM returns read_file call
Given a mock LLM caller that makes one read_file tool call
When ToolCallingAgent.process is called with prompt "Review src/auth.py"
Then the tool_calls count is 1
And the final response is non-empty
# ---------- B: multi-turn tool loop ----------
Scenario: Multi-turn tool loop — mock LLM makes two consecutive tool calls
Given a mock LLM caller that makes 2 consecutive tool calls before finishing
When ToolCallingAgent.process is called with prompt "Analyse two files"
Then the tool_calls count is 2
And the loop ran for at least 3 iterations
# ---------- C: no-skill plain LLM regression ----------
Scenario: No-skill plain LLM — SimpleLLMAgent used, no tool schemas sent
Given a SimpleLLMAgent with a mock LLM
When SimpleLLMAgent.process is called with a simple prompt
Then SimpleLLMAgent returns the mocked response
And no tool schemas were sent to the LLM
# ---------- D: skill tools not silently dropped ----------
Scenario: Skill tools not silently dropped when actor has no base tools list
Given an actor config with no base tools list
And resolved skill tool entries for the actor
When _make_agent_instance is called for that actor
Then the result is a ToolCallingAgent instance
# ---------- E: ToolCallingLLMCaller first-call system prompt ----------
Scenario: ToolCallingLLMCaller sends system prompt on first call
Given a ToolCallingLLMCaller with an actor config containing a system_prompt
When invoke is called for the first time with a user prompt
Then the accumulated messages include a SystemMessage
And the accumulated messages include a HumanMessage
# ---------- F: ToolCallingLLMCaller subsequent call appends tool results ----------
Scenario: ToolCallingLLMCaller appends tool results on subsequent call
Given a ToolCallingLLMCaller with an actor config containing a system_prompt
And a prior first invoke has been made
When invoke is called again with tool_results
Then a ToolMessage is appended to the accumulated messages
# ---------- G: GraphExecutor passes context to ToolCallingAgent ----------
Scenario: GraphExecutor._invoke_agent passes context dict to ToolCallingAgent
Given a ToolCallingAgent spy that captures its process() arguments
When GraphExecutor._invoke_agent is called with that agent and a context dict
Then the context dict was forwarded to ToolCallingAgent.process
# ---------- H: last_run_tool_calls property reflects run results ----------
Scenario: ReactiveCleverAgentsApp.last_run_tool_calls reflects tool calls
Given a ReactiveCleverAgentsApp with a registered ToolCallingAgent that has last_result
When _tally_tool_calls is called on the app
Then last_run_tool_calls equals the number of tool call history entries
# ---------- I: ToolCallingLLMCaller _render_prompt ----------
Scenario: ToolCallingLLMCaller._render_prompt handles empty template
Given a fresh ToolCallingLLMCaller
When _render_prompt is called with an empty string template
Then _render_prompt returns an empty string
Scenario: ToolCallingLLMCaller._render_prompt renders Jinja2 template
Given a fresh ToolCallingLLMCaller
When _render_prompt is called with a Jinja2 template and context
Then _render_prompt returns the rendered string
Scenario: ToolCallingLLMCaller._render_prompt handles rendering exception
Given a fresh ToolCallingLLMCaller
When _render_prompt is called with a template that raises during rendering
Then _render_prompt returns the original template
# ---------- J: ToolCallingLLMCaller _resolve_llm with kwargs ----------
Scenario: ToolCallingLLMCaller._resolve_llm passes temperature max_tokens max_retries to provider
Given a ToolCallingLLMCaller with temperature max_tokens max_retries in actor_config
When _resolve_llm is called with tool_schemas
Then the provider registry create_llm was called with the config kwargs
Scenario: ToolCallingLLMCaller._resolve_llm skips bind_tools when no schemas
Given a ToolCallingLLMCaller with temperature max_tokens max_retries in actor_config
When _resolve_llm is called without tool_schemas
Then bind_tools was not called on the LLM
Scenario: ToolCallingLLMCaller._resolve_llm falls back when bind_tools raises
Given a ToolCallingLLMCaller with a provider that raises on bind_tools
When _resolve_llm is called with tool_schemas for a failing bind
Then the plain LLM is used without tool binding
# ---------- K: ToolCallingLLMCaller invoke no system prompt ----------
Scenario: ToolCallingLLMCaller.invoke without system prompt only adds HumanMessage
Given a ToolCallingLLMCaller with an actor config without system_prompt
When invoke is called for the first time with a user prompt
Then the accumulated messages include a HumanMessage
And no SystemMessage was added
# ---------- L: ToolCallingLLMCaller invoke list content ----------
Scenario: ToolCallingLLMCaller.invoke handles list content in response
Given a ToolCallingLLMCaller with an actor config without system_prompt
When invoke is called and the LLM returns list content
Then the response content joins the list parts
# ---------- M: ToolCallingLLMCaller invoke failed tool result ----------
Scenario: ToolCallingLLMCaller.invoke uses error field on failed tool result
Given a ToolCallingLLMCaller with an actor config containing a system_prompt
And a prior first invoke has been made
When invoke is called with a failed tool_result
Then a ToolMessage with the error text is appended
# ---------- N: ToolCallingAgent._build_tool_registry edge cases ----------
Scenario: ToolCallingAgent._build_tool_registry skips empty tool name
Given a ToolCallingAgent with an entry that has an empty name
When _build_tool_registry is called
Then the local registry is empty
Scenario: ToolCallingAgent._build_tool_registry skips duplicate names
Given a ToolCallingAgent with two entries for the same builtin tool
When _build_tool_registry is called
Then the local registry has exactly one tool registered
Scenario: ToolCallingAgent._build_tool_registry warns when tool not in builtin registry
Given a ToolCallingAgent with an entry not found in builtin registry
When _build_tool_registry is called
Then the local registry is empty
# ---------- O: ToolCallingAgent.process_message_sync ----------
Scenario: ToolCallingAgent.process_message_sync delegates to process
Given a ToolCallingAgent with no tools and a mock process method
When process_message_sync is called
Then the result is the same as calling process directly
# ---------- P: ToolCallingLLMCaller.invoke returns tool calls from response ----------
Scenario: ToolCallingLLMCaller.invoke extracts tool calls from LLM response
Given a ToolCallingLLMCaller with an actor config without system_prompt
When invoke is called and the LLM returns a response with tool call dicts
Then the LLMResponse contains the extracted tool calls
# ---------- Q: _resolve_provider_format maps provider to correct format ----------
Scenario: _resolve_provider_format returns ANTHROPIC for anthropic provider
Given an actor config with provider "anthropic"
When _resolve_provider_format is called with that actor config
Then _resolve_provider_format returns ProviderFormat.ANTHROPIC
Scenario: _resolve_provider_format returns OPENAI for openai provider
Given an actor config with provider "openai"
When _resolve_provider_format is called with that actor config
Then _resolve_provider_format returns ProviderFormat.OPENAI
Scenario: _resolve_provider_format returns OPENAI for unknown provider
Given an actor config with provider "google"
When _resolve_provider_format is called with that actor config
Then _resolve_provider_format returns ProviderFormat.OPENAI
Scenario: _resolve_provider_format returns OPENAI for empty actor config
Given an empty actor config
When _resolve_provider_format is called with that actor config
Then _resolve_provider_format returns ProviderFormat.OPENAI
Scenario: _resolve_provider_format returns OPENAI for None actor config
When _resolve_provider_format is called with actor_config None
Then _resolve_provider_format returns ProviderFormat.OPENAI
# ---------- R: ToolCallingAgent.process passes provider_format to runtime ----------
Scenario: ToolCallingAgent.process passes provider_format to ToolCallingRuntime
Given a ToolCallingAgent with an anthropic actor config and a file-read tool entry
When ToolCallingAgent.process is called with prompt "Read my file"
Then ToolCallingRuntime was constructed with ProviderFormat.ANTHROPIC
# ---------- S: Tool name encoding/decoding ----------
Scenario: _encode_tool_name replaces colon and slash with uppercase sentinels
When _encode_tool_name is called with "server:namespace/tool"
Then _encode_tool_name returns "server_C_namespace_S_tool"
Scenario: _encode_tool_name is idempotent on already-clean names
When _encode_tool_name is called with "my-clean_tool"
Then _encode_tool_name returns "my-clean_tool"
Scenario: _decode_tool_name restores colon and slash from sentinels
When _decode_tool_name is called with "server_C_namespace_S_tool"
Then _decode_tool_name returns "server:namespace/tool"
Scenario: _decode_tool_name is idempotent on clean names
When _decode_tool_name is called with "my-clean_tool"
Then _decode_tool_name returns "my-clean_tool"
Scenario: encode-decode round-trip preserves original name
Given a tool name "local/my__tool"
When the tool name is encoded then decoded
Then the round-trip name equals "local/my__tool"
Scenario: encode-decode round-trip for server-qualified name
Given a tool name "server.example:builtin/git-status"
When the tool name is encoded then decoded
Then the round-trip name equals "server.example:builtin/git-status"
# ---------- T: _resolve_llm encodes tool names in schemas ----------
Scenario: _resolve_llm encodes tool names before calling bind_tools
Given a ToolCallingLLMCaller with default actor config
When _resolve_llm is called with a schema containing a namespaced tool name
Then bind_tools was called with the encoded tool name
# ---------- U: invoke decodes tool names from LLM response ----------
Scenario: invoke decodes tool names when extracting tool calls from LLM response
Given a ToolCallingLLMCaller with an actor config without system_prompt
When invoke is called and the LLM returns a response with encoded tool call names
Then the LLMResponse contains the decoded tool calls
# ---------- S: Tool name encoding/decoding ----------
Scenario: _encode_tool_name replaces colon and slash with uppercase sentinels
When _encode_tool_name is called with "server:namespace/tool"
Then _encode_tool_name returns "server_C_namespace_S_tool"
Scenario: _encode_tool_name is idempotent on already-clean names
When _encode_tool_name is called with "my-clean_tool"
Then _encode_tool_name returns "my-clean_tool"
Scenario: _decode_tool_name restores colon and slash from sentinels
When _decode_tool_name is called with "server_C_namespace_S_tool"
Then _decode_tool_name returns "server:namespace/tool"
Scenario: _decode_tool_name is idempotent on clean names
When _decode_tool_name is called with "my-clean_tool"
Then _decode_tool_name returns "my-clean_tool"
Scenario: encode-decode round-trip preserves original name
Given a tool name "local/my__tool"
When the tool name is encoded then decoded
Then the round-trip name equals "local/my__tool"
Scenario: encode-decode round-trip for server-qualified name
Given a tool name "server.example:builtin/git-status"
When the tool name is encoded then decoded
Then the round-trip name equals "server.example:builtin/git-status"
# ---------- T: _resolve_llm encodes tool names in schemas ----------
Scenario: _resolve_llm encodes tool names before calling bind_tools
Given a ToolCallingLLMCaller with default actor config
When _resolve_llm is called with a schema containing a namespaced tool name
Then bind_tools was called with the encoded tool name
# ---------- U: invoke decodes tool names from LLM response ----------
Scenario: invoke decodes tool names when extracting tool calls from LLM response
Given a ToolCallingLLMCaller with an actor config without system_prompt
When invoke is called and the LLM returns a response with encoded tool call names
Then the LLMResponse contains the decoded tool calls
# ---------- V: ToolCallingLLMCaller options block forwarding (#11243) ----------
@tdd_issue @tdd_issue_11243
Scenario: ToolCallingLLMCaller forwards openai_api_base and openai_api_key from options block
Given a ToolCallingLLMCaller with actor config options containing openai_api_base and openai_api_key
When _resolve_llm is called on the ToolCallingLLMCaller with empty tool_schemas
Then create_llm was called with openai_api_base forwarded from options
And create_llm was called with __api_key_sentinel extracted from options openai_api_key
@tdd_issue @tdd_issue_11243
Scenario: ToolCallingLLMCaller forwards allowed options keys to create_llm
Given a ToolCallingLLMCaller with actor config options containing allowed extra keys
When _resolve_llm is called on the ToolCallingLLMCaller with empty tool_schemas
Then create_llm was called with the allowed options keys forwarded
@tdd_issue @tdd_issue_11243
Scenario: ToolCallingLLMCaller rejects reserved keys in options block with a warning
Given a ToolCallingLLMCaller with actor config options containing reserved key provider_type
When _resolve_llm is called on the ToolCallingLLMCaller with empty tool_schemas
Then create_llm was NOT called with the reserved key provider_type
@tdd_issue @tdd_issue_11243
Scenario: ToolCallingLLMCaller rejects unknown keys in options block with a warning
Given a ToolCallingLLMCaller with actor config options containing unknown key foo_bar
When _resolve_llm is called on the ToolCallingLLMCaller with empty tool_schemas
Then create_llm was NOT called with the unknown key foo_bar
@tdd_issue @tdd_issue_11243
Scenario: ToolCallingLLMCaller top-level config takes precedence over options block
Given a ToolCallingLLMCaller with actor config with top-level temperature and options temperature
When _resolve_llm is called on the ToolCallingLLMCaller with empty tool_schemas
Then create_llm was called with the top-level temperature value