08
Human-in-the-Loop + Wizard
Финал

У нас есть skills для файлов, bash и рассуждений. Теперь добавляем ещё один: interaction — три tool'а для общения с пользователем. Добавить human-in-the-loop = добавить ещё один skill. Agent loop не меняется.

Два связанных паттерна в одном:

Human-in-the-loop — агент останавливается и спрашивает подтверждение перед опасными действиями (запись файлов).

Wizard/Guide — агент анализирует, генерирует варианты, предлагает выбор, и так несколько шагов.

Три интерактивных tool'а
// Модель сама решает какой тип взаимодействия нужен:

// 1. Выбор из вариантов (wizard-стиль)
{
    name: "ask_user_choice",
    description: "Предложить пользователю выбор из вариантов",
    parameters: {
        question: "Описание ситуации",
        options: [
            { label: "Вариант A", description: "Плюсы, минусы" },
            { label: "Вариант B", description: "Плюсы, минусы" },
        ],
        allow_custom: true
    }
}

// 2. Текстовый ввод
{ name: "ask_user_input" }    // "Как назвать проект?"

// 3. Подтверждение да/нет
{ name: "ask_user_confirm" }  // "Записать файл?"

Шаг 1. Создаём interaction skill

Три tool'а для общения с пользователем — это skill, такой же как filesystem или bash. Модель не знает, что tools интерактивные. Для неё ask_user_choice — обычный tool, который возвращает строку "Пользователь выбрал: ...". Вся магия в handlers.

Создаём структуру
mkdir -p skills/interaction
skills/interaction/SKILL.md
---
name: interaction
description: Ask the user questions, offer choices, and request confirmations. Use when the agent needs human input or approval.
metadata:
  author: ai-agents-book
  version: "1.0"
---

# Interaction Skill

Ты можешь взаимодействовать с пользователем: задавать вопросы,
предлагать варианты, запрашивать подтверждения.

## Доступные инструменты
- **ask_user_choice** — предложить выбор из нескольких вариантов
- **ask_user_input** — запросить текстовый ввод
- **ask_user_confirm** — запросить подтверждение да/нет

## Правила
- Предлагай 2-4 варианта с описаниями плюсов и минусов
- Используй ask_user_confirm перед необратимыми действиями
- Не делай предположений — спрашивай
- Разбивай сложные решения на шаги (wizard-паттерн)

Async handlers — ключевой момент

Handlers для interaction skill — асинхронные. Они блокируют выполнение и ждут ввод от пользователя через readline. Наш createAgent() из skill-utils уже поддерживает async handlers (помнишь Promise<string> в типе?). Поэтому agent loop не надо менять — он просто await'ит результат.

wrapWithConfirmation — паттерн композиции

Human-in-the-loop для write_file — это обёртка поверх существующего handler'а. Берём filesystem skill, оборачиваем его write_file handler: сначала показываем превью + спрашиваем подтверждение, потом вызываем оригинальный handler. Skill не меняется — меняется только handler.

src/08-interactive-agent.ts
// src/08-interactive-agent.ts — Human-in-the-loop + Wizard
import {
    discoverSkills, activateSkill, createAgent,
    createFilesystemSkill, createBashSkill, createReasoningSkill,
    createInteractionSkill, Skill,
} from "./skill-utils";

import * as readline from "readline";

// === Утилита для ввода ===
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const ask = (q: string): Promise<string> =>
    new Promise(resolve => rl.question(q, answer => resolve(answer.trim())));

// === wrapWithConfirmation: обёртка для human-in-the-loop ===
function wrapWithConfirmation(skill: Skill, toolName: string): Skill {
    const originalHandler = skill.handlers[toolName];
    return {
        ...skill,
        handlers: {
            ...skill.handlers,
            [toolName]: async (args: any) => {
                console.log("\n" + "═".repeat(50));
                console.log(`⚠️  Агент хочет: ${toolName}(${JSON.stringify(args).slice(0, 80)})`);

                // Превью содержимого для write_file
                if (args.content) {
                    console.log("─".repeat(50));
                    const lines = args.content.split("\n");
                    console.log(lines.slice(0, 12).join("\n"));
                    if (lines.length > 12) console.log(`... (ещё ${lines.length - 12} строк)`);
                }

                console.log("─".repeat(50));
                const answer = await ask("Разрешить? [y/n]: ");

                if (["y", "д", "да", "yes"].includes(answer.toLowerCase())) {
                    const result = originalHandler(args);
                    const output = result instanceof Promise ? await result : result;
                    return `✅ ${output} (одобрено пользователем)`;
                }
                return `❌ Пользователь отклонил действие ${toolName}`;
            },
        },
    };
}

// === Загрузка skills ===
function loadSkills(): Skill[] {
    const discovered = discoverSkills("./skills");
    console.log("\n🔌 Activation:");
    const skills: Skill[] = [];

    for (const d of discovered) {
        const meta = activateSkill(d.location);
        switch (meta.name) {
            case "filesystem":
                // Оборачиваем write_file в human-in-the-loop
                skills.push(wrapWithConfirmation(createFilesystemSkill(meta), "write_file"));
                break;
            case "bash":
                skills.push(createBashSkill(meta));
                break;
            case "reasoning":
                skills.push(createReasoningSkill(meta));
                break;
            case "interaction":
                skills.push(createInteractionSkill(meta));
                break;
        }
    }

    console.log(`\n🚀 ${skills.length} skills: ${skills.map(s => s.name).join(", ")}`);
    return skills;
}

// === Интерактивный агент ===
async function interactiveAgent(task: string, systemPrompt?: string) {
    const skills = loadSkills();

    const defaultPrompt = `Ты умный ассистент-визард. Вот твои навыки:

<available_skills>
{{SKILLS}}
</available_skills>

СТИЛЬ РАБОТЫ:
1. Разберись в ситуации (read_file, run_bash, think)
2. Подготовь варианты
3. Предложи выбор через ask_user_choice
4. Уточни детали через ask_user_input
5. Перед изменениями — ask_user_confirm или write_file (с подтверждением)

Не делай предположений — спрашивай. Предлагай 2-4 варианта.`;

    const agent = createAgent(skills, {
        systemPrompt: systemPrompt || defaultPrompt,
        maxIterations: 30,
    });

    await agent(task);
    rl.close();
}

// === Сценарии ===
async function main() {
    const scenario = process.argv[2] ?? "project";

    switch (scenario) {
        case "project":
            await interactiveAgent(
                "Помоги создать новый TypeScript проект. " +
                "Спроси что за проект, предложи структуру, настрой всё."
            );
            break;
        case "refactor":
            await interactiveAgent(
                "Проанализируй .ts файлы в src/ и предложи рефакторинг. " +
                "Покажи варианты, дай выбрать, выполни с подтверждениями."
            );
            break;
        case "free":
            console.log("🤖 Интерактивный агент. Введите задачу:\n");
            const task = await ask("> ");
            await interactiveAgent(task);
            break;
        default:
            console.log("Сценарии: npx tsx src/08-interactive-agent.ts [project|refactor|free]");
            rl.close();
    }
}

main();

Главная хитрость: модель не знает что эти tool'ы интерактивные. Для неё ask_user_choice — обычный tool, который возвращает строку "Пользователь выбрал: ...". Вся магия в handlers — они блокируют выполнение и ждут ввод через readline.

Демо в формате диалога
1/4
Пример вывода. У тебя может быть иначе — это нормально.
npx tsx src/08-interactive-agent.ts project
npx tsx src/08-interactive-agent.ts refactor
npx tsx src/08-interactive-agent.ts free
✅ Что должно получиться
Агент задаёт вопросы (ask_user_choice, ask_user_input), ждёт ввод пользователя, и запрашивает подтверждение перед записью файлов (wrapWithConfirmation). Все 4 skills работают вместе: filesystem, bash, reasoning, interaction.
🧯 Если не работает
Запускай из терминала (не из браузера). Проверь, что skills/interaction/SKILL.md создан и что readline/STDIN доступны.
💡 Главная идея: Модель не знает, что эти tool'ы интерактивные. Для неё ask_user_choice — обычный tool, который возвращает строку "Пользователь выбрал: ...". Вся магия в handlers — они блокируют выполнение и ждут ввод через readline. Модель думает, что просто вызвала функцию и получила результат.

wrapWithConfirmation — паттерн композиции. Берём существующий skill (filesystem), оборачиваем один handler (write_file) в подтверждение. Skill не меняется — меняется только обработчик. Это тот же принцип что и middleware в Express.

Async handlerscreateAgent() поддерживает их из коробки. Добавить human-in-the-loop = добавить skill + обёртку. Agent loop остаётся тем же.