Files
ss-tools/docs/adr/ADR-0008-assistant-tool-registry.md
busya 05ef6cdff8 docs(validation): update agent configs, skills, ADRs, specs
- qa-tester: add validation v2 testing checklist
- speckit.plan: add component reuse scan for frontend
- molecular-cot-logging: add Svelte logger + CLI reader
- semantics-svelte: add RSM model-first protocol, frontend stack
- templates: update plan/tasks/ux-reference templates
- ADR-0004: add llm_dashboard_validation plugin registration
- ADR-0008: add assistant tool registry for v2 validation
- Specs: update 017 tasks from 51 to 99
2026-05-31 22:32:32 +03:00

7.4 KiB
Raw Blame History

[DEF:ADR-0008:ADR]

@STATUS ACTIVE

@PURPOSE Replace the manual five-file tool registration (tool catalog, dispatch, permissions, safe-ops, command parser) with a single @assistant_tool decorator-based registry that automatically generates the LLM catalog, dispatches operations, and enforces permissions — eliminating cross-file desynchronisation when adding new assistant commands.

@RELATION DEPENDS_ON -> [ADR-0001:ADR]

@RELATION DEPENDS_ON -> [AssistantSchemas]

@RELATION CALLS -> [AssistantToolRegistry]

@RATIONALE The current architecture requires editing 45 separate files per new tool: _build_tool_catalog() (tool metadata), _dispatch_intent() (routing), INTENT_PERMISSION_CHECKS (permissions), _SAFE_OPS (safety classification), and optionally _parse_command() (keyword fallback). In practice, developers (both human and AI) frequently miss one of these, resulting in tools that appear in the LLM catalog but fail at dispatch, or dispatch successfully but bypass permission checks. A decorator-based registry collocates all tool metadata with its handler, making it impossible to register an incomplete tool. The registry also enables automatic catalog generation, eliminating the hand-maintained _build_tool_catalog().

@REJECTED Centralised YAML/JSON config file (e.g. tools.yaml) — rejected because it separates tool metadata from implementation, requiring developers to jump between config and code. It also cannot express permission-check logic or handler imports, so it would still need a parallel registration step, reducing the benefit.

@REJECTED Automatic discovery via filesystem scanning (e.g. importlib scanning _tool_*.py) — rejected because it makes the tool set implicit; adding a _tool_*.py file silently registers a tool, which is dangerous for guarded or destructive operations. Explicit decorator registration in a known _tool_registry.py module keeps the tool set fully visible and auditable.

@REJECTED Incremental improvement of existing pattern (add one more dict, add one more if) — rejected because it does not solve the root problem: the five-file scatter. Any new file or dict that must be kept in sync with the handler will eventually desynchronise.

@DATA_CONTRACT Handler[Intent, User, TaskManager, ConfigManager, Session] -> [text: str, task_id: str | None, actions: list[AssistantAction]]

Decision

1. New file: _tool_registry.py

A central registry module that provides:

@assistant_tool(
    operation="search_dashboards",
    domain="dashboards",
    description="Search and list dashboards by name, id, or get all dashboards",
    optional_entities=["search", "environment", "page", "page_size"],
    risk_level="safe",
    requires_confirmation=False,
    permission_checks=[("dashboards", "READ")],
)
async def handle_search_dashboards(intent, user, task_manager, config_manager, db):
    ...

The decorator stores the tool definition in a module-level dict[str, AssistantTool] and attaches it as func.__assistant_tool__ for optional introspection.

2. Auto-generated catalog

_build_tool_catalog() is replaced by get_catalog(user) which:

  1. Iterates all registered tools
  2. Filters by user permissions via _has_any_permission()
  3. Returns LLM-ready dict list

3. Auto-dispatched routing

_dispatch_intent() is replaced by dispatch(operation, ...) which:

  1. Looks up operation in the registry
  2. Calls tool.handler(intent, user, task_manager, config_manager, db)
  3. Returns (text, task_id, actions)

4. Derived lists

_SAFE_OPS is derived from risk_level == "safe" in the registry. INTENT_PERMISSION_CHECKS is derived from tool.permission_checks. Both hardcoded dicts are removed.

5. Migration strategy

Each existing tool handler is moved from _dispatch.py:_dispatch_intent() into its own _tool_*.py file with an @assistant_tool decorator. Tools are migrated one at a time, in this order:

Step Tool File
1 show_capabilities _tool_capabilities.py
2 get_task_status _tool_task_status.py
3 create_branch _tool_create_branch.py
4 commit_changes _tool_commit.py
5 deploy_dashboard _tool_deploy.py
6 execute_migration _tool_migration.py
7 run_backup _tool_backup.py
8 run_llm_validation _tool_llm_validation.py
9 run_llm_documentation _tool_llm_documentation.py
10 get_health_summary _tool_health_summary.py

After migration, _dispatch.py is reduced to a thin dispatch() wrapper. The _parse_command() function is updated to also use the registry for keyword-to-operation mapping.

Step 8 update (v2): run_llm_validation now creates a persistent validation task (policy) via POST /api/validation-tasks (i.e. ValidationTaskService.create_task()) and triggers an immediate run via ValidationTaskService.trigger_run(), replacing the previous ad-hoc task_manager.create_task(plugin_id="llm_dashboard_validation") dispatch. This makes LLM validation repeatable, schedulable, and auditable — each run is tracked as a ValidationRun with pass/fail/warn counts.

6. File layout after migration

backend/src/api/routes/assistant/
├── __init__.py
├── _admin_routes.py          # admin endpoints (list_conversations, delete, history, audit)
├── _routes.py                # main router (send_message, confirm, cancel)
├── _tool_registry.py         # @assistant_tool decorator, get_catalog(), dispatch()
├── _tool_capabilities.py
├── _tool_task_status.py
├── _tool_create_branch.py
├── _tool_commit.py
├── _tool_deploy.py
├── _tool_migration.py
├── _tool_backup.py
├── _tool_llm_validation.py
├── _tool_llm_documentation.py
├── _tool_health_summary.py
├── _schemas.py               # Pydantic models, in-memory stores
├── _history.py               # Conversation history helpers
├── _dispatch.py              # removed or thin wrapper
├── _llm_planner.py           # removed _build_tool_catalog, uses get_catalog()
├── _llm_planner_intent.py
├── _command_parser.py
├── _resolvers.py
├── _dataset_review.py
└── _dataset_review_dispatch.py

Consequences

Positive

  • Single point of registration — adding a tool requires editing exactly one file: the tool handler with its @assistant_tool decorator
  • No desync — catalog, dispatch, permissions, and safety classification all derive from the same decorator metadata
  • Testable_tool_registry.py can be unit-tested without DB or HTTP fixtures
  • Discoverablefrom _tool_registry import _tools gives a complete inventory of all registered tools
  • Safe-by-default — a tool without @assistant_tool is not registered and cannot be invoked
  • Task-based validationrun_llm_validation now creates persistent validation tasks (policies) via ValidationTaskService, making each validation run repeatable, schedulable, and auditable through the ValidationRun aggregate model

Negative

  • One-time migration cost — 10 existing handlers must be moved to new files (~200 LOC total)
  • Import overhead — each _tool_*.py must be imported somewhere to trigger decorator registration (solved by importing them in __init__.py or _tool_registry.py)
  • Dynamic dispatch — replaces static if/elif with dict lookup, which is marginally less explicit (mitigated by strong typing of AssistantTool dataclass)

[/DEF:ADR-0008:ADR]