9ec578f229
CI / lint (pull_request) Successful in 44s
CI / typecheck (pull_request) Successful in 49s
CI / security (pull_request) Successful in 47s
CI / quality (pull_request) Successful in 33s
CI / unit_tests (pull_request) Successful in 3m4s
CI / integration_tests (pull_request) Successful in 1m2s
CI / build (pull_request) Successful in 33s
CI / coverage (pull_request) Successful in 3m5s
CI / status-check (pull_request) Successful in 2s
CI / lint (push) Successful in 31s
CI / typecheck (push) Successful in 46s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 31s
CI / unit_tests (push) Successful in 3m2s
CI / integration_tests (push) Successful in 1m2s
CI / build (push) Successful in 31s
CI / coverage (push) Successful in 3m4s
CI / status-check (push) Successful in 6s
Extends AgentFactory.__init__ and create_executor() with a keyword-only
tool_agent_class: type[ToolAgent] = ToolAgent parameter so callers can
inject a custom ToolAgent subclass at runtime without monkey-patching
module globals (resolves the five-module setattr loop in cleveragents-webapp).
**AgentFactory** (agents/factory.py):
- Accepts tool_agent_class keyword argument; validates it is a ToolAgent
subclass at construction time (fail-fast, CONTRIBUTING.md §Argument
Validation).
- Stores as self._tool_agent_class and uses it to populate
self.agent_types['tool'], so every create_agent('tool') call
instantiates the supplied subclass.
- Forwards tool_agent_class to LLMAgent when creating 'llm'-typed agents
so the LLM tool-calling loop honours the injected subclass.
- Default ToolAgent is preserved, so all existing callers are unaffected.
**LLMAgent** (agents/llm.py):
- Accepts a keyword-only tool_agent_class: type[ToolAgent] = ToolAgent
argument, validated as a ToolAgent subclass at construction time.
- The multi-turn tool-call loop (_execute_tool_loop) constructs its
ephemeral tool executors via self._tool_agent_class(...) across all
three dispatch sites (regular loop, budget-exhaustion synthesis round,
and stuck-model synthesis round), so the LLM tool-calling path no
longer falls back to the module-level ToolAgent. This closes the gap
that previously prevented removing the webapp monkey-patch for the LLM
tool-calling path.
**Executor / create_executor** (runtime.py):
- Executor.__init__ accepts and validates tool_agent_class; stores as
self.tool_agent_class.
- create_executor() accepts tool_agent_class and forwards it to Executor.
**Dispatch paths** (runtime_dispatch.py):
- _execute_tool: uses executor.tool_agent_class(...) instead of the
module-level ToolAgent(...) so the direct-construction path (bypassing
AgentFactory) also honours the injected subclass.
- _execute_graph and _execute_graph_stream: pass
tool_agent_class=executor.tool_agent_class to AgentFactory so graph
nodes using tool-typed agents pick up the subclass.
- _execute_llm and _execute_llm_stream: pass
tool_agent_class=executor.tool_agent_class to AgentFactory so the
single-LLM actor's internally-created LLMAgent (and its tool-calling
loop) honours the injected subclass. Without this, a single LLM actor
that makes tool calls silently fell back to the base ToolAgent.
- _execute_multi_actor: passes tool_agent_class=executor.tool_agent_class
to the sub-Executor so every actor inside a multi-actor bundle (tool,
graph, or LLM) honours the injected subclass. Without this, all
dispatch paths reachable through a multi-actor bundle fell back to the
base ToolAgent.
- Removed the now-unused module-level ToolAgent import; the _execute_tool
docstring cross-reference is fully qualified so it still resolves.
All six dispatch paths that construct or dispatch tool agents now
forward the injected subclass.
**ReactiveCleverAgentsApp** (core/application.py):
- Removed the redundant register_agent_type('tool', ToolAgent) call from
load_configuration and the dead type_map re-registration block in
_create_agents; AgentFactory.__init__ already owns the 'tool'
registration. This drops the last hardcoded module-level ToolAgent
references flagged in the issue #73 audit.
**Tests** (features/tool_agent_class_injection.feature + steps):
- 17 BDD scenarios covering: default class when parameter omitted;
custom subclass stored on factory and executor; invalid class rejected
with ConfigurationError (factory, executor, and LLMAgent); factory
create_agent returns custom instance; _execute_tool dispatch
instantiates custom subclass; the LLM tool loop instantiates the
injected subclass; isinstance LSP compatibility through the factory;
the _execute_llm dispatch path forwarding tool_agent_class to
AgentFactory (single-LLM actor making a tool call); and the
_execute_multi_actor dispatch path forwarding tool_agent_class to the
sub-Executor. The two new dispatch-path scenarios were verified to
fail without the source fixes (instantiation_count=0), confirming
they guard the injection contract for the single-LLM and multi-actor
paths.
**Updated tests** (features/steps/runtime_coverage_steps.py,
features/steps/runtime_extended_coverage_steps.py):
- Removed the now-dead patch('cleveractors.runtime_dispatch.ToolAgent')
mocks (and their mock instances) that no longer intercept _execute_tool
after it switched to executor.tool_agent_class. The tool-actor scenario
continues to pass via the real ToolAgent; the normal-coverage branch
uses patch.object(ToolAgent, 'process_message', ...) to stub tool
execution.
**Updated test** (features/steps/credential_executor_paths_steps.py):
- The _execute_multi_actor credentials-forwarding scenario intercepts
Executor.__init__ with a capturing_init to assert the credentials
dict reaches the sub-Executor. Now that _execute_multi_actor forwards
tool_agent_class to the sub-Executor, capturing_init accepts and
forwards that keyword so the assertion continues to pass.
**Removed obsolete test** (features/application_coverage_gaps.feature):
- Dropped the '_create_agents registers built-in agent types' scenario
and its step definitions; the behaviour it asserted (redundant
re-registration) was removed from _create_agents.
ISSUES CLOSED: #73
119 lines
7.1 KiB
Gherkin
119 lines
7.1 KiB
Gherkin
Feature: tool_agent_class injection in AgentFactory and create_executor
|
|
As a platform developer consuming cleveractors-core
|
|
I want to supply a custom ToolAgent subclass to AgentFactory and create_executor
|
|
So that I can extend tool agent behaviour without monkey-patching module globals
|
|
|
|
# ── AgentFactory ────────────────────────────────────────────────────────────
|
|
|
|
Scenario: AgentFactory defaults to ToolAgent when tool_agent_class is omitted
|
|
Given a minimal AgentFactory config with a single tool agent
|
|
When I create an AgentFactory without tool_agent_class
|
|
Then the factory agent_types["tool"] entry should be the default ToolAgent
|
|
|
|
Scenario: AgentFactory uses custom subclass when tool_agent_class is provided
|
|
Given a minimal AgentFactory config with a single tool agent
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I create an AgentFactory with that custom tool_agent_class
|
|
Then the factory agent_types["tool"] entry should be the custom subclass
|
|
|
|
Scenario: AgentFactory stores custom subclass on _tool_agent_class attribute
|
|
Given a minimal AgentFactory config with a single tool agent
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I create an AgentFactory with that custom tool_agent_class
|
|
Then the factory _tool_agent_class attribute should be the custom subclass
|
|
|
|
Scenario: AgentFactory rejects non-ToolAgent class as tool_agent_class
|
|
Given a minimal AgentFactory config with a single tool agent
|
|
When I create an AgentFactory with a non-ToolAgent class as tool_agent_class
|
|
Then factory creation should raise ConfigurationError containing "subclass of ToolAgent"
|
|
|
|
Scenario: AgentFactory creates tool agent using the custom subclass
|
|
Given a minimal AgentFactory config with a single tool agent named "my_tool"
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I create an AgentFactory with that custom tool_agent_class
|
|
And I create the agent named "my_tool" from the factory
|
|
Then the created agent should be an instance of the custom subclass
|
|
|
|
Scenario: AgentFactory default behaviour is unchanged (no regression)
|
|
Given a minimal AgentFactory config with a single tool agent named "default_tool"
|
|
When I create an AgentFactory without tool_agent_class
|
|
And I create the agent named "default_tool" from the factory
|
|
Then the created agent should be an instance of ToolAgent
|
|
|
|
# ── Executor and create_executor ────────────────────────────────────────────
|
|
|
|
Scenario: Executor defaults to ToolAgent when tool_agent_class is omitted
|
|
Given a valid Executor config dict
|
|
When I create an Executor without tool_agent_class
|
|
Then the executor tool_agent_class attribute should be the default ToolAgent
|
|
|
|
Scenario: Executor stores custom subclass on tool_agent_class attribute
|
|
Given a valid Executor config dict
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I create an Executor with that custom tool_agent_class
|
|
Then the executor tool_agent_class attribute should be the custom subclass
|
|
|
|
Scenario: Executor rejects non-ToolAgent class as tool_agent_class
|
|
Given a valid Executor config dict
|
|
When I create an Executor with a non-ToolAgent class as tool_agent_class
|
|
Then executor creation should raise ConfigurationError containing "subclass of ToolAgent"
|
|
|
|
Scenario: create_executor defaults to ToolAgent when tool_agent_class is omitted
|
|
Given a valid Executor config dict
|
|
When I call create_executor without tool_agent_class
|
|
Then the returned executor tool_agent_class should be the default ToolAgent
|
|
|
|
Scenario: create_executor forwards custom subclass to Executor
|
|
Given a valid Executor config dict
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I call create_executor with that custom tool_agent_class
|
|
Then the returned executor tool_agent_class should be the custom subclass
|
|
|
|
# ── _execute_tool dispatch path ──────────────────────────────────────────────
|
|
|
|
Scenario: _execute_tool uses custom subclass when provided via Executor
|
|
Given a valid tool Executor config dict
|
|
And a custom ToolAgent subclass named "CustomToolAgent" that records instantiation
|
|
When I execute the tool actor using the custom subclass
|
|
Then the custom subclass should have been instantiated for tool execution
|
|
|
|
# ── _execute_llm dispatch path (issue #73: AgentFactory forwarding) ─────────
|
|
|
|
Scenario: _execute_llm forwards tool_agent_class so the LLM tool loop uses the custom subclass
|
|
Given a single-llm Executor config with tools
|
|
And a custom ToolAgent subclass named "RecordingToolAgent" that records instantiation
|
|
When I execute the single-llm Executor with a mock chat model that returns one tool call then text
|
|
Then the custom subclass should have been instantiated for tool execution
|
|
|
|
# ── _execute_multi_actor dispatch path (issue #73: sub-Executor forwarding) ──
|
|
|
|
Scenario: _execute_multi_actor forwards tool_agent_class to the sub-Executor
|
|
Given a multi-actor Executor config with a tool sub-actor
|
|
And a custom ToolAgent subclass named "RecordingToolAgent" that records instantiation
|
|
When I execute the multi-actor Executor using the custom subclass
|
|
Then the custom subclass should have been instantiated for tool execution
|
|
|
|
# ── LLM agent tool loop (issue #73: ephemeral tool executors) ───────────────
|
|
|
|
Scenario: LLMAgent tool loop uses the injected tool_agent_class subclass
|
|
Given an AgentFactory config with a single llm agent that has tools
|
|
And a custom ToolAgent subclass named "RecordingToolAgent" that records instantiation
|
|
When I create an AgentFactory with that custom tool_agent_class
|
|
And I create the agent named "llm_with_tools" from the factory
|
|
And I run the llm agent tool loop with a mock chat model that returns one tool call then text
|
|
Then the custom subclass should have been instantiated for tool execution
|
|
|
|
Scenario: LLMAgent rejects a non-ToolAgent tool_agent_class at construction
|
|
Given a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I construct an LLMAgent directly with a non-ToolAgent tool_agent_class
|
|
Then LLMAgent construction should raise ConfigurationError containing "subclass of ToolAgent"
|
|
|
|
# ── isinstance compatibility ─────────────────────────────────────────────────
|
|
|
|
Scenario: Factory-created custom tool agent is an instance of ToolAgent (LSP)
|
|
Given a minimal AgentFactory config with a single tool agent named "lsp_tool"
|
|
And a custom ToolAgent subclass named "CustomToolAgent"
|
|
When I create an AgentFactory with that custom tool_agent_class
|
|
And I create the agent named "lsp_tool" from the factory
|
|
Then the created agent should be an instance of ToolAgent
|