У нас есть skills для файлов, bash и рассуждений. Теперь добавляем ещё один:
interaction — три tool'а для общения с пользователем.
Добавить human-in-the-loop = добавить ещё один skill. Agent loop не меняется.
Два связанных паттерна в одном:
Human-in-the-loop — агент останавливается и спрашивает подтверждение перед опасными действиями (запись файлов).
Wizard/Guide — агент анализирует, генерирует варианты, предлагает выбор, и так несколько шагов.
// Модель сама решает какой тип взаимодействия нужен: // 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
--- 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 — 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.
npx tsx src/08-interactive-agent.ts project
npx tsx src/08-interactive-agent.ts refactor
npx tsx src/08-interactive-agent.ts free
skills/interaction/SKILL.md
создан и что readline/STDIN доступны.
ask_user_choice — обычный tool, который возвращает строку
"Пользователь выбрал: ...".
Вся магия в handlers — они блокируют выполнение и ждут ввод через readline.
Модель думает, что просто вызвала функцию и получила результат.
wrapWithConfirmation — паттерн композиции. Берём существующий skill (filesystem), оборачиваем один handler (write_file) в подтверждение. Skill не меняется — меняется только обработчик. Это тот же принцип что и middleware в Express.
Async handlers —
createAgent() поддерживает их из коробки.
Добавить human-in-the-loop = добавить skill + обёртку. Agent loop остаётся тем же.