Skip to content

Commit fb59613

Browse files
bouillipxclaude
andcommitted
feat(tool): add built-in ask_user tool for LLM-driven user input
Add AskUserTool as a framework-built-in tool that is automatically registered for every agent, allowing the LLM to ask structured questions when it needs user clarification, decisions, or approval. - AskUserTool: ITool implementation with structured question schema - IUserInputHandler: callback protocol for host apps (CLI, web, etc.) - CLIUserInputHandler: default stdin/stdout implementation - Builder auto-registers ask_user; no manual add_tools() needed - with_user_input_handler() on builder for custom handler injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4a6b645 commit fb59613

6 files changed

Lines changed: 478 additions & 0 deletions

File tree

‎dare_framework/agent/_internal/builder.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
IValidatorManager,
5656
)
5757
from dare_framework.tool._internal.managers.tool_manager import ToolManager
58+
from dare_framework.tool._internal.tools.ask_user import AskUserTool, IUserInputHandler
5859
from dare_framework.tool.interfaces import ITool, IToolProvider, RunContext
5960
from dare_framework.tool.kernel import IExecutionControl, IToolGateway, IToolManager
6061

@@ -102,6 +103,9 @@ def __init__(self, name: str) -> None:
102103
# Initial skill path (None = not set; single path for one skill).
103104
self._initial_skill_path: str | None = None
104105

106+
# User input handler for the built-in ask_user tool.
107+
self._user_input_handler: IUserInputHandler | None = None
108+
105109
def _manager_config(self) -> Config | None:
106110
"""Return the Config passed to managers."""
107111

@@ -209,6 +213,15 @@ def with_skill(self: TBuilder, path: str | Path) -> TBuilder:
209213
self._initial_skill_path = str(path)
210214
return self
211215

216+
def with_user_input_handler(self: TBuilder, handler: IUserInputHandler) -> TBuilder:
217+
"""Inject a custom user-input handler for the built-in ``ask_user`` tool.
218+
219+
If not called, the default ``CLIUserInputHandler`` (stdin/stdout) is
220+
used. Pass a custom implementation for web UIs, Slack bots, etc.
221+
"""
222+
self._user_input_handler = handler
223+
return self
224+
212225
async def build(self) -> Any:
213226
"""Build the agent. When with_config() was used and config.mcp_paths is set, loads MCP inside build()."""
214227
if self._config is not None and getattr(self._config, "mcp_paths", None):
@@ -300,6 +313,8 @@ def _ensure_tool_provider(
300313
# Include knowledge tool names so they are always listed when knowledge is set.
301314
if self._resolved_knowledge() is not None:
302315
explicit_names = explicit_names | {"knowledge_get", "knowledge_add"}
316+
# Built-in ask_user is always available.
317+
explicit_names = explicit_names | {"ask_user"}
303318
return _ConfiguredToolProvider(
304319
manager=manager,
305320
config=self._config,
@@ -329,6 +344,9 @@ def _register_tools_with_manager(self, manager: IToolManager | None, tools: list
329344
if knowledge is not None:
330345
manager.register_tool(KnowledgeGetTool(knowledge))
331346
manager.register_tool(KnowledgeAddTool(knowledge))
347+
# Built-in ask_user tool — always available so the LLM can request user input.
348+
ask_user_tool = AskUserTool(handler=self._user_input_handler)
349+
manager.register_tool(ask_user_tool)
332350

333351
def _resolved_model(self) -> IModelAdapter:
334352
if self._model is not None:

‎dare_framework/tool/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737

3838
# Built-in v4 tool runtime helpers and file tools
3939
from dare_framework.tool._internal import (
40+
AskUserTool,
41+
CLIUserInputHandler,
42+
IUserInputHandler,
4043
EditLineTool,
4144
FileExecutionControl,
4245
NoOpMCPClient,
@@ -87,6 +90,9 @@
8790
"NoOpTool",
8891
"ToolManager",
8992
# v4 tool runtime helpers and file tools
93+
"AskUserTool",
94+
"CLIUserInputHandler",
95+
"IUserInputHandler",
9096
"NoOpMCPClient",
9197
"NoOpSkill",
9298
"RunCommandTool",

‎dare_framework/tool/_internal/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
from dare_framework.tool._internal.adapters.noop_mcp_client import NoOpMCPClient
1414
from dare_framework.tool._internal.toolkits.mcp_toolkit import MCPToolkit
1515
from dare_framework.tool._internal.managers.tool_manager import ToolManager
16+
from dare_framework.tool._internal.tools.ask_user import (
17+
AskUserTool,
18+
CLIUserInputHandler,
19+
IUserInputHandler,
20+
)
1621
from dare_framework.tool._internal.tools.echo_tool import EchoTool
1722
from dare_framework.tool._internal.tools.noop_tool import NoopTool
1823
from dare_framework.tool._internal.tools.noop_skill import NoOpSkill
@@ -24,6 +29,9 @@
2429
from dare_framework.tool._internal.utils.run_context_state import RunContextState
2530

2631
__all__ = [
32+
"AskUserTool",
33+
"CLIUserInputHandler",
34+
"IUserInputHandler",
2735
"Checkpoint",
2836
"DefaultExecutionControl",
2937
"FileExecutionControl",

‎dare_framework/tool/_internal/tools/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
"""Built-in tool implementations for the tool domain."""
22

3+
from dare_framework.tool._internal.tools.ask_user import (
4+
AskUserTool,
5+
CLIUserInputHandler,
6+
IUserInputHandler,
7+
)
38
from dare_framework.tool._internal.tools.echo_tool import EchoTool
49
from dare_framework.tool._internal.tools.noop_skill import NoOpSkill
510
from dare_framework.tool._internal.tools.noop_tool import NoopTool
@@ -10,6 +15,9 @@
1015
from dare_framework.tool._internal.tools.edit_line import EditLineTool
1116

1217
__all__ = [
18+
"AskUserTool",
19+
"CLIUserInputHandler",
20+
"IUserInputHandler",
1321
"EchoTool",
1422
"NoOpSkill",
1523
"NoopTool",

0 commit comments

Comments
 (0)