Примечание.
Второй пилот 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 репозитории.