Skip to content

Elicitation Patterns

Elicitation is the pattern of asking the user for clarification, confirmation, or a choice at runtime. This is the cornerstone of "human-in-the-loop" (HITL) workflows, ensuring that high-stakes or ambiguous decisions are validated by a human.

aifred-tk provides the elicit_choice function, which is environment-aware:

  • In MCP clients (like Claude Code), it shows a native GUI dialog.
  • In the CLI, it uses an interactive terminal prompt.

For detailed API documentation, see Elicitation Core API.

Pattern: Safety Confirmation

Before performing a destructive or broad-reaching action (like deleting files or pushing to production), always ask for confirmation.

from aifred_tk.elicitation import Choice, elicit_choice
from aifred_tk.core.interfaces import ToolResult

def execute(self, args: ToolArgs) -> ToolResult:
    # ... logic that prepared a destructive change ...

    choice = elicit_choice(
        question="Are you sure you want to delete these 50 files?",
        choices=[
            Choice("yes", description="Proceed with deletion", example="Delete files"),
            Choice("no", description="Abort the operation")
        ],
        context="Deleting these files might break the build if they are still referenced."
    )

    if choice.selected_label != "yes":
        return ToolResult(output={"status": "cancelled", "message": "User aborted deletion."})

    # ... proceed with deletion ...
    return ToolResult(output={"status": "deleted"}, made_changes=True)

Pattern: Refinement (Free Input)

If the user's initial input is ambiguous or insufficient, use elicitation to ask for more details.

def execute(self, args: ToolArgs) -> ToolResult:
    if not args.search_query:
        choice = elicit_choice(
            "What exactly are you looking for?",
            [Choice("skip", "Never mind")],
            allow_free_input=True
        )

        if choice.is_free_input:
            query = choice.free_text
        else:
            return ToolResult(output={"status": "skipped"})
    # ...

Pattern: Strategic Branching

Use elicitation to let the user decide which path the tool should take after an initial analysis.

def execute(self, args: ToolArgs) -> ToolResult:
    # 1. Analyze code
    # 2. Present options to the user
    choice = elicit_choice(
        "I found a bottleneck. How should I proceed?",
        [
            Choice("optimize", "Apply the suggested optimization"),
            Choice("refactor", "Restructure the class instead"),
            Choice("ignore", "I'll handle it later")
        ]
    )

    match choice.selected_label:
        case "optimize": 
            # self._apply_optimization returns a ToolResult
            return self._apply_optimization(...)
        case "refactor": 
            # self._apply_refactoring returns a ToolResult
            return self._apply_refactoring(...)
        case _: 
            return ToolResult(output={"status": "ignored"})