Skip to main content

Работа с крючками

Используйте крючки для настройки поведения ваших Второй пилот SDK сессий.

Кто может использовать эту функцию?

GitHub Copilot SDK Доступна со всеми Copilot тарифными планами.

Примечание.

          Второй пилот SDK в настоящее время находится в Technical Preview. Функциональность и доступность могут меняться.

Hooks позволяют вставлять пользовательскую логику на каждый этап Второй пилот SDK сессии — от момента её начала, через каждую пользовательскую подсказку и вызов инструмента, до момента её завершения. Вы можете использовать крючки для реализации прав, аудита, уведомлений и прочего, не изменяя поведение основного агента.

Обзор

Крюк — это обратный вызов, который вы регистрируете один раз при создании сессии. SDK вызывает его в определённой точке жизненного цикла разговора, передаёт контекстный ввод и, по желанию, принимает выводы, изменяющие поведение сессии. Для подробной диаграммы последовательности сессионного потока см. репозиторийgithub/copilot-sdk.

ОбработчикКогда он срабатываетЧто можно сделать
onSessionStartСессия начинается (новая или возобновлённая)Инжекция контекста, настройки загрузки
onUserPromptSubmittedПользователь отправляет сообщениеПереписывайте подсказки, добавляйте контекст, фильтруйте вводные данные
onPreToolUseДо запуска инструментаРазрешить, отклонить или изменить вызов
onPostToolUseПосле возвращения инструментаТрансформировать результаты, редактировать секреты, аудитировать
onSessionEndСессия завершенаОчистить, зафиксировать метрики
onErrorOccurredВозникает ошибкаПользовательский логинг, логика повторных попыток, оповещения

Все крючки необязательны — регистрируйте только те, которые вам нужны. Возврат null (или языковой эквивалент) с любого хука говорит SDK продолжать поведение по умолчанию.

Регистрационные крючки

Передайте объект hooks при создании (или возобновлении) сессии. Каждый приведённый пример следует этой схеме.

import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
await client.start();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => { /* ... */ },
        onPreToolUse:   async (input, invocation) => { /* ... */ },
        onPostToolUse:  async (input, invocation) => { /* ... */ },
        // ... add only the hooks you need
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Примеры в Python, Go и .NET см. репозиторийgithub/copilot-sdk.

Совет

Каждый обработчик хука получает invocation параметр, содержащий sessionId, что полезно для корреляции логов и поддержания состояния за сессию.

Контроль разрешений

Используйте onPreToolUse для создания слоя разрешений, который определяет, какие инструменты агент может запускать, какие аргументы разрешены и следует ли запросить пользователя перед выполнением.

Список разрешений на безопасный набор инструментов

const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (!READ_ONLY_TOOLS.includes(input.toolName)) {
                return {
                    permissionDecision: "deny",
                    permissionDecisionReason:
                        `Only read-only tools are allowed. "${input.toolName}" was blocked.`,
                };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Примеры в Python, Go и .NET см. репозиторийgithub/copilot-sdk.

Ограничить доступ к файлам определёнными каталогами

const ALLOWED_DIRS = ["/home/user/projects", "/tmp"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (["read_file", "write_file", "edit"].includes(input.toolName)) {
                const filePath = (input.toolArgs as { path: string }).path;
                const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));

                if (!allowed) {
                    return {
                        permissionDecision: "deny",
                        permissionDecisionReason:
                            `Access to "${filePath}" is outside the allowed directories.`,
                    };
                }
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Спросите пользователя перед деструктивными операциями

const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Возврат "ask" делегирует решение пользователю во время выполнения — это полезно для разрушительных действий, когда вы хотите, чтобы человек был в цикле.

Аудит и соблюдение требований

Объедините onPreToolUse, onPostToolUse, и жизненный цикл сессии зацепляется, чтобы создать полный аудит, который фиксирует каждое действие агента.

Журнал структурированного аудита

interface AuditEntry {
    timestamp: number;
    sessionId: string;
    event: string;
    toolName?: string;
    toolArgs?: unknown;
    toolResult?: unknown;
    prompt?: string;
}

const auditLog: AuditEntry[] = [];

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_start",
            });
            return null;
        },
        onUserPromptSubmitted: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "user_prompt",
                prompt: input.prompt,
            });
            return null;
        },
        onPreToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_call",
                toolName: input.toolName,
                toolArgs: input.toolArgs,
            });
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_result",
                toolName: input.toolName,
                toolResult: input.toolResult,
            });
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_end",
            });

            // Persist the log — swap this with your own storage backend
            await fs.promises.writeFile(
                `audit-${invocation.sessionId}.json`,
                JSON.stringify(auditLog, null, 2),
            );
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Для примера в Python см. github/copilot-sdk репозиторий.

Скрыть секреты из результатов инструментов

const SECRET_PATTERNS = [
    /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi,
];

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            if (typeof input.toolResult !== "string") return null;

            let redacted = input.toolResult;
            for (const pattern of SECRET_PATTERNS) {
                redacted = redacted.replace(pattern, "[REDACTED]");
            }

            return redacted !== input.toolResult
                ? { modifiedResult: redacted }
                : null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Уведомления

Крючки срабатывают в процессе вашего приложения, поэтому вы можете вызвать любые побочные эффекты, такие как уведомления на рабочем столе, звуки, сообщения в Slack или звонки webhook.

Уведомления на рабочем столе о событиях сессии

import notifier from "node-notifier"; // npm install node-notifier

const session = await client.createSession({
    hooks: {
        onSessionEnd: async (input, invocation) => {
            notifier.notify({
                title: "Copilot Session Complete",
                message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,
            });
            return null;
        },
        onErrorOccurred: async (input) => {
            notifier.notify({
                title: "Copilot Error",
                message: input.error.slice(0, 200),
            });
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Для примера в Python см. github/copilot-sdk репозиторий.

Воспроизведите звук, когда инструмент заканчивается

import { exec } from "node:child_process";

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            // macOS: play a system sound after every tool call
            exec("afplay /System/Library/Sounds/Pop.aiff");
            return null;
        },
        onErrorOccurred: async () => {
            exec("afplay /System/Library/Sounds/Basso.aiff");
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Опубликовать об ошибках в Slack

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input, invocation) => {
            if (!input.recoverable) {
                await fetch(SLACK_WEBHOOK_URL, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``,
                    }),
                });
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Быстрое обогащение

Используйте onSessionStart и onUserPromptSubmitted автоматически вводите контекст, чтобы пользователям не приходилось повторяться.

Ввести метаданные проекта при начале сессии

import * as fs from "node:fs";

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            const pkg = JSON.parse(
                await fs.promises.readFile("package.json", "utf-8"),
            );
            return {
                additionalContext: [
                    `Project: ${pkg.name} v${pkg.version}`,
                    `Node: ${process.version}`,
                    `CWD: ${input.cwd}`,
                ].join("\n"),
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Расширяйте команды стенографии в подсказках

const SHORTCUTS: Record<string, string> = {
    "/fix":      "Find and fix all errors in the current file",
    "/test":     "Write comprehensive unit tests for this code",
    "/explain":  "Explain this code in detail",
    "/refactor": "Refactor this code to improve readability",
};

const session = await client.createSession({
    hooks: {
        onUserPromptSubmitted: async (input) => {
            for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {
                if (input.prompt.startsWith(shortcut)) {
                    const rest = input.prompt.slice(shortcut.length).trim();
                    return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };
                }
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Обработка ошибок и восстановление

Крючок onErrorOccurred даёт вам шанс реагировать на неудачи — будь то повторная попытка, уведомление человека или достойное отключение.

Повторяйте ошибки переходных моделей

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            if (input.errorContext === "model_call" && input.recoverable) {
                return {
                    errorHandling: "retry",
                    retryCount: 3,
                    userNotification: "Temporary model issue—retrying…",
                };
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Дружественные сообщения об ошибках

const FRIENDLY_MESSAGES: Record<string, string> = {
    model_call:      "The AI model is temporarily unavailable. Please try again.",
    tool_execution:  "A tool encountered an error. Check inputs and try again.",
    system:          "A system error occurred. Please try again later.",
};

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            return {
                userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Метрики сеанса

Отслеживайте, сколько длятся сессии, сколько инструментов запускается и почему сессии заканчиваются — это полезно для дашбордов и мониторинга затрат.

const metrics = new Map<string, { start: number; toolCalls: number; prompts: number }>();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            metrics.set(invocation.sessionId, {
                start: input.timestamp,
                toolCalls: 0,
                prompts: 0,
            });
            return null;
        },
        onUserPromptSubmitted: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.prompts++;
            return null;
        },
        onPreToolUse: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.toolCalls++;
            return { permissionDecision: "allow" };
        },
        onSessionEnd: async (input, invocation) => {
            const m = metrics.get(invocation.sessionId)!;
            const durationSec = (input.timestamp - m.start) / 1000;

            console.log(
                `Session ${invocation.sessionId.slice(0, 8)}: ` +
                `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +
                `${m.toolCalls} tool calls, ended: ${input.reason}`,
            );

            metrics.delete(invocation.sessionId);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Для примера в Python см. github/copilot-sdk репозиторий.

Комбинирование крюков

Хуки составляются естественно. Один hooks объект может обрабатывать права, аудит и уведомления — каждый крючок выполняет свою функцию.

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            console.log(`[audit] session started in ${input.cwd}`);
            return { additionalContext: "Project uses TypeScript and Vitest." };
        },
        onPreToolUse: async (input) => {
            console.log(`[audit] tool requested: ${input.toolName}`);
            if (input.toolName === "shell") {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input) => {
            console.log(`[audit] tool completed: ${input.toolName}`);
            return null;
        },
        onErrorOccurred: async (input) => {
            console.error(`[alert] ${input.errorContext}: ${input.error}`);
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            console.log(`[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

Лучшие практики

  •         **Держите крючки быстрыми.** Каждый крючок идёт по очереди — медленные хуки задерживают разговор. По возможности переложите тяжёлую работу (запись в базу данных, HTTP-вызовы) в фоновую очередь.
    
  •         **Возвращайся `null` , когда тебе нечего менять.** Это указывает SDK продолжать по умолчанию и избегает ненужного распределения объектов.
    
  •         **Будьте чёткие в решениях о разрешении.** Возврат `{ permissionDecision: "allow" }` яснее, чем возврат `null`, хотя оба позволяют использовать инструмент.
    
  •         **Не принимайте критические ошибки.** Можно подавить ошибки восстанавливаемых инструментов, но всегда логируйте или оповещайте о невосстановимых ошибках.
    
  •         **Используйте `additionalContext` вместо того, `modifiedPrompt` чтобы иметь возможность.** Добавление контекста сохраняет изначальный замысел пользователя, при этом направляя модель.
    
  •         **Состояние области действия по идентификатору сессии.** Если вы отслеживаете данные за сессию, включайте `invocation.sessionId` их и очищайте в `onSessionEnd`.
    

Дополнительные материалы

Для получения дополнительной информации см. ссылку на Hooks в github/copilot-sdk репозитории.