@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
