1
0

feat: migrate to Agno native HITL tool confirmation flow (#158)

Replace the homebrew regex-based tool extraction and manual dispatch
(tool_executor.py) with Agno's built-in Human-In-The-Loop confirmation:

- Toolkit(requires_confirmation_tools=...) marks dangerous tools
- agent.run() returns RunOutput with status=paused when confirmation needed
- RunRequirement.confirm()/reject() + agent.continue_run() resumes execution

Dashboard and Discord vendor both use the native flow. DuckDuckGo import
isolated so its absence doesn't kill all tools. Test stubs cleaned up
(agno is a real dependency, only truly optional packages stubbed).

1384 tests pass in parallel (~14s).

Co-authored-by: Trip T <trip@local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-09 21:54:04 -04:00
committed by GitHub
parent 574031a55c
commit 904a7c564e
18 changed files with 1317 additions and 85 deletions

View File

@@ -26,7 +26,6 @@ logger = logging.getLogger(__name__)
_ImportError = None
try:
from agno.tools import Toolkit
from agno.tools.duckduckgo import DuckDuckGoTools
from agno.tools.file import FileTools
from agno.tools.python import PythonTools
from agno.tools.shell import ShellTools
@@ -36,6 +35,15 @@ except ImportError as e:
_AGNO_TOOLS_AVAILABLE = False
_ImportError = e
# DuckDuckGo is optional — don't let it kill all tools
try:
from agno.tools.duckduckgo import DuckDuckGoTools
_DUCKDUCKGO_AVAILABLE = True
except ImportError:
_DUCKDUCKGO_AVAILABLE = False
DuckDuckGoTools = None # type: ignore[assignment, misc]
# Track tool usage stats
_TOOL_USAGE: dict[str, list[dict]] = {}
@@ -142,8 +150,9 @@ def create_research_tools(base_dir: str | Path | None = None):
toolkit = Toolkit(name="research")
# Web search via DuckDuckGo
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
if _DUCKDUCKGO_AVAILABLE:
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
# File reading
from config import settings
@@ -262,8 +271,9 @@ def create_data_tools(base_dir: str | Path | None = None):
toolkit.register(file_tools.list_files, name="list_files")
# Web search for finding datasets
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
if _DUCKDUCKGO_AVAILABLE:
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
return toolkit
@@ -301,8 +311,9 @@ def create_security_tools(base_dir: str | Path | None = None):
toolkit.register(shell_tools.run_shell_command, name="shell")
# Web search for threat intelligence
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
if _DUCKDUCKGO_AVAILABLE:
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
# File reading for logs/configs
base_path = Path(base_dir) if base_dir else Path(settings.repo_root)
@@ -403,11 +414,20 @@ def create_full_toolkit(base_dir: str | Path | None = None):
if not _AGNO_TOOLS_AVAILABLE:
# Return None when tools aren't available (tests)
return None
toolkit = Toolkit(name="full")
# Web search
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
from timmy.tool_safety import DANGEROUS_TOOLS
toolkit = Toolkit(
name="full",
requires_confirmation_tools=list(DANGEROUS_TOOLS),
)
# Web search (optional — degrades gracefully if ddgs not installed)
if _DUCKDUCKGO_AVAILABLE:
search_tools = DuckDuckGoTools()
toolkit.register(search_tools.web_search, name="web_search")
else:
logger.info("DuckDuckGo tools unavailable (ddgs not installed) — skipping web_search")
# Python execution
python_tools = PythonTools()