07
Adaptive Agent — Plan + ReAct + Replan
Продвинутый

Урок 6 показал две стратегии, но обе были фиксированы. Что если реальность отличается от плана? Мы добавляем request_replanсистемный tool (не skill!), который позволяет агенту запросить перепланирование. Важное различие: skills — переиспользуемые способности; системные tools — механизмы архитектуры агента.

Plan ReAct Execute Replan? Execute Done

Это и есть то, как работают реальные агенты. Три фазы в цикле:

Фаза 1: Plan — отдельный LLM-вызов без tools. Модель составляет структурированный JSON-план, не зная реального состояния системы.

Фаза 2: ReAct Execute — выполняет план с think tool. На каждом шаге думает: «совпадает ли реальность с планом?» Если мелкая проблема — адаптируется на лету.

Фаза 3: Replan — если агент вызвал request_replan, цикл возвращается к планированию. Но теперь планировщик знает: что уже сделано, что нового обнаружили, почему старый план не подходит.

request_replan — системный tool, не skill

Почему request_replan — не skill? Потому что это механизм agent loop, а не переиспользуемая способность. Skill — это то, что агент умеет делать (читать файлы, искать в интернете, думать). request_replan — это то, как агент управляет своим рабочим процессом. Его нельзя переиспользовать в другом контексте без самого planning loop.

Мы добавляем его через extraTools в createAgent() — именно для этого мы расширяли options в skill-utils.

Системный tool vs Skill
// Skill — переиспользуемая способность:
//   skills/reasoning/SKILL.md → think tool
//   skills/bash/SKILL.md      → run_bash tool
//   Любой агент может их подключить

// Системный tool — механизм архитектуры:
//   request_replan → управляет planning loop
//   Существует только внутри createPlanningAgent()

// Добавляем через extraTools, не через SKILL.md
const agent = createAgent(skills, {
    extraTools: [replanTool],
    extraHandlers: { request_replan: () => "REPLAN_REQUESTED" },
});

Skills из предыдущих уроков

Новых SKILL.md файлов нет — мы переиспользуем те же skills из уроков 5 и 6: filesystem, bash, reasoning. Вся новая логика — в agent loop и system prompt.

src/07-adaptive-agent.ts
// src/07-adaptive-agent.ts — Adaptive: Plan + ReAct + Replan
import OpenAI from "openai";
import {
    discoverSkills, activateSkill, createAgent,
    createFilesystemSkill, createBashSkill, createReasoningSkill,
    Skill
} from "./skill-utils";

import dotenv from "dotenv";
dotenv.config();

const client = new OpenAI({
    baseURL: "https://openrouter.ai/api/v1",
    apiKey: process.env.OPENROUTER_API_KEY ?? "",
});
const MODEL = "anthropic/claude-sonnet-4.5";

// === request_replan — СИСТЕМНЫЙ tool, не skill ===
const replanTool: OpenAI.ChatCompletionTool = {
    type: "function",
    function: {
        name: "request_replan",
        description:
            "Вызови когда план нужно пересмотреть: новая информация, " +
            "провал шага, или лучший путь к цели.",
        parameters: {
            type: "object",
            properties: {
                reason: { type: "string", description: "Почему нужен новый план" },
                completed_steps: {
                    type: "array", items: { type: "string" },
                    description: "Какие шаги уже выполнены",
                },
                discoveries: {
                    type: "array", items: { type: "string" },
                    description: "Что нового узнали",
                },
            },
            required: ["reason", "completed_steps", "discoveries"],
        },
    },
};

// === Загрузка skills (те же что в уроке 6) ===
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": skills.push(createFilesystemSkill(meta)); break;
            case "bash":       skills.push(createBashSkill(meta)); break;
            case "reasoning":  skills.push(createReasoningSkill(meta)); break;
        }
    }
    console.log(`\n🚀 ${skills.length} skills: ${skills.map(s => s.name).join(", ")}`);
    return skills;
}

// === ФАЗА 1: Планирование ===
interface Plan {
    goal: string;
    steps: { id: number; action: string; tool: string; reason: string }[];
}

async function createPlan(
    task: string,
    context?: { previousPlan?: Plan; completedSteps?: string[]; discoveries?: string[] }
): Promise<Plan> {
    let systemPrompt = `Ты стратегический планировщик. Составь план.
Доступные tools: run_bash, read_file, write_file, list_dir, think.
Ответь ТОЛЬКО валидным JSON:
{ "goal": "цель", "steps": [{ "id": 1, "action": "...", "tool": "...", "reason": "..." }] }`;

    if (context?.previousPlan) {
        systemPrompt += `\n\nКОНТЕКСТ РЕПЛАНА:
Предыдущий план: ${JSON.stringify(context.previousPlan)}
Уже выполнено: ${context.completedSteps?.join(", ") || "ничего"}
Новые данные: ${context.discoveries?.join("; ") || "нет"}
Учти что уже сделано, не повторяй выполненные шаги.`;
    }

    const response = await client.chat.completions.create({
        model: MODEL,
        messages: [
            { role: "system", content: systemPrompt },
            { role: "user", content: task },
        ],
    });

    const text = response.choices[0].message.content ?? "";
    return JSON.parse(text.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim());
}

// === ФАЗА 2: Выполнение с ReAct + replan ===
interface ExecutionResult {
    status: "completed" | "replan_requested";
    replanContext?: { reason: string; completedSteps: string[]; discoveries: string[] };
}

async function executeWithReact(skills: Skill[], task: string, plan: Plan): Promise<ExecutionResult> {
    // Специальный handler для request_replan
    let replanData: ExecutionResult["replanContext"] | null = null;

    const agent = createAgent(skills, {
        systemPrompt: `Ты исполнитель с аналитическим мышлением.

ТВОЙ ПЛАН:
${JSON.stringify(plan, null, 2)}

МЕТОД: think → act → think → act.
Если план нужно менять — вызови request_replan.
НЕ вызывай replan для мелочей — адаптируйся на лету.

<available_skills>
{{SKILLS}}
</available_skills>`,
        maxIterations: 20,
        extraTools: [replanTool],
        extraHandlers: {
            request_replan: (args: any) => {
                replanData = {
                    reason: args.reason,
                    completedSteps: args.completed_steps,
                    discoveries: args.discoveries,
                };
                console.log(`\n  🔄 РЕПЛАН: ${args.reason}`);
                return "REPLAN_REQUESTED";
            },
        },
    });

    await agent(task);

    if (replanData) {
        return { status: "replan_requested", replanContext: replanData };
    }
    return { status: "completed" };
}

// === Главный цикл: Plan → Execute → Replan? ===
async function adaptiveAgent(task: string) {
    console.log("╔══════════════════════════════════════════════╗");
    console.log("║   ADAPTIVE AGENT: Plan + ReAct + Replan      ║");
    console.log("╚══════════════════════════════════════════════╝");
    console.log(`\n📌 Задача: ${task}\n`);

    const skills = loadSkills();

    let plan = await createPlan(task);
    console.log(`\n📋 ПЛАН: ${plan.goal}`);
    plan.steps.forEach(s => console.log(`  ${s.id}. [${s.tool}] ${s.action}`));

    const MAX_REPLANS = 3;
    let replanCount = 0;

    while (replanCount <= MAX_REPLANS) {
        console.log(`\n⚡ ВЫПОЛНЕНИЕ (план v${replanCount + 1})\n`);
        const result = await executeWithReact(skills, task, plan);

        if (result.status === "completed") {
            console.log("\n🎉 Агент завершил задачу.");
            return;
        }

        replanCount++;
        if (replanCount > MAX_REPLANS) {
            console.log("\n⚠️  Лимит репланов. Завершаем.");
            return;
        }

        console.log(`\n📋 РЕПЛАН #${replanCount}...`);
        plan = await createPlan(task, {
            previousPlan: plan,
            completedSteps: result.replanContext?.completedSteps,
            discoveries: result.replanContext?.discoveries,
        });
        console.log(`🔄 НОВЫЙ ПЛАН: ${plan.goal}`);
        plan.steps.forEach(s => console.log(`  ${s.id}. [${s.tool}] ${s.action}`));
    }
}

// === Запуск ===
const task = process.argv[2] ||
    "Проанализируй все .ts файлы в директории src/. " +
    "Найди дублирование кода между файлами. " +
    "Создай файл REFACTORING.md с рекомендациями.";

adaptiveAgent(task);
npx tsx src/07-adaptive-agent.ts
Демо в формате диалога
1/1
Пример вывода. У тебя может быть иначе — это нормально.
✅ Что должно получиться
Видно три фазы: Discovery/Activation skills, планирование, пошаговое выполнение. Агент может запросить реплан, но не зацикливается (лимит MAX_REPLANS=3).
🧯 Если не работает
Проверь, что request_replan передан через extraTools и что extraHandlers содержит его обработчик.
💡 Skill vs системный tool — не всё должно быть skill'ом. Skills — переиспользуемые способности (filesystem, bash, reasoning). Системные tools — механизмы архитектуры (request_replan). Разница: skill живёт в SKILL.md и работает в любом агенте. Системный tool существует только внутри конкретного agent loop.

Adaptive replanning: планировщик при реплане получает контекст — что уже сделано, что нового узнали, почему старый план не подходит. Это отличает адаптивного агента от тупого исполнителя.

extraTools в createAgent() — точка расширения. Мы не меняли skill-utils.ts — просто передали системный tool через options.