Skip to main content

후크를 사용한 작업

후크를 사용하면 시작되는 순간부터 각 사용자 프롬프트와 도구 호출을 거쳐 종료되는 순간까지 Copilot 세션의 모든 단계에 사용자 지정 논리를 연결할 수 있습니다. 이 가이드에서는 핵심 에이전트 동작을 수정하지 않고 사용 권한, 감사, 알림 등을 제공할 수 있도록 실제 사용 사례를 안내합니다.

Overview

후크는 세션을 만들 때 한 번 등록하는 콜백입니다. SDK는 대화 수명 주기의 잘 정의된 지점에서 호출하고, 상황별 입력을 전달하며, 필요에 따라 세션의 동작을 수정하는 출력을 수락합니다.

다이어그램: 설명된 프로세스를 보여 주는 순서도입니다.

후크실행 시수행 가능한 작업
세션 수명 주기 후크세션 시작(신규 또는 다시 시작)컨텍스트 삽입, 부하 기본 설정
사용자 프롬프트 제출 후크사용자가 메시지를 보냅니다.프롬프트 다시 작성, 컨텍스트 추가, 입력 필터링
사전 도구 사용 후크도구를 실행하기 전에호출 허용/거부/수정
사후 도구 사용 후크도구가 반환된 후(성공에만 해당)결과 변환, 기밀 편집, 감사
사후 도구 사용 후크도구에서 오류를 반환한 후재시도 지침 삽입, 로그 오류
세션 수명 주기 후크세션 종료클린업, 메트릭 기록하기
오류 처리 후크오류가 발생합니다.사용자 지정 로깅, 다시 시도 논리, 경고

모든 후크는 선택 사항이며 필요한 후크만 등록합니다. null (또는 해당 언어의 동등한 표현)을 후크에서 반환하면 SDK가 기본 동작을 계속합니다.

후크 등록

hooks 세션을 만들거나 다시 시작할 때 개체를 전달합니다. 아래의 모든 예제는 이 패턴을 따릅니다.

TypeScript
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: "approve-once" }),
});
Python
from copilot import CopilotClient, PermissionDecisionApproveOnce

client = CopilotClient()
await client.start()

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_pre_tool_use":  on_pre_tool_use,
        "on_post_tool_use": on_post_tool_use,
        # ... add only the hooks you need
    },
)
Go
package main

import (
    "context"
    copilot "github.com/github/copilot-sdk/go"
    "github.com/github/copilot-sdk/go/rpc"
)

func onSessionStart(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) {
    return nil, nil
}

func onPreToolUse(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
    return nil, nil
}

func onPostToolUse(input copilot.PostToolUseHookInput, inv copilot.HookInvocation) (*copilot.PostToolUseHookOutput, error) {
    return nil, nil
}

func main() {
    ctx := context.Background()
    client := copilot.NewClient(nil)

    session, err := client.CreateSession(ctx, &copilot.SessionConfig{
        Hooks: &copilot.SessionHooks{
            OnSessionStart: onSessionStart,
            OnPreToolUse:   onPreToolUse,
            OnPostToolUse:  onPostToolUse,
        },
        OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
            return &rpc.PermissionDecisionApproveOnce{}, nil
        },
    })
    _ = session
    _ = err
}
client := copilot.NewClient(nil)

session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    Hooks: &copilot.SessionHooks{
        OnSessionStart: onSessionStart,
        OnPreToolUse:   onPreToolUse,
        OnPostToolUse:  onPostToolUse,
        // ... add only the hooks you need
    },
    OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
        return &rpc.PermissionDecisionApproveOnce{}, nil
    },
})
.NET
using GitHub.Copilot;
using GitHub.Copilot.Rpc;

public static class HooksExample
{
    static Task<SessionStartHookOutput?> onSessionStart(SessionStartHookInput input, HookInvocation invocation) =>
        Task.FromResult<SessionStartHookOutput?>(null);
    static Task<PreToolUseHookOutput?> onPreToolUse(PreToolUseHookInput input, HookInvocation invocation) =>
        Task.FromResult<PreToolUseHookOutput?>(null);
    static Task<PostToolUseHookOutput?> onPostToolUse(PostToolUseHookInput input, HookInvocation invocation) =>
        Task.FromResult<PostToolUseHookOutput?>(null);

    public static async Task Main()
    {
        var client = new CopilotClient();

        var session = await client.CreateSessionAsync(new SessionConfig
        {
            Hooks = new SessionHooks
            {
                OnSessionStart = onSessionStart,
                OnPreToolUse   = onPreToolUse,
                OnPostToolUse  = onPostToolUse,
            },
            OnPermissionRequest = (req, inv) =>
                Task.FromResult(PermissionDecision.ApproveOnce()),
        });
    }
}
var client = new CopilotClient();

var session = await client.CreateSessionAsync(new SessionConfig
{
    Hooks = new SessionHooks
    {
        OnSessionStart = onSessionStart,
        OnPreToolUse   = onPreToolUse,
        OnPostToolUse  = onPostToolUse,
        // ... add only the hooks you need
    },
    OnPermissionRequest = (req, inv) =>
        Task.FromResult(PermissionDecision.ApproveOnce()),
});
Java
import com.github.copilot.sdk.CopilotClient;
import com.github.copilot.sdk.events.*;
import com.github.copilot.sdk.json.*;
import java.util.concurrent.CompletableFuture;

try (var client = new CopilotClient()) {
    client.start().get();

    var hooks = new SessionHooks()
        .setOnSessionStart((input, inv) -> CompletableFuture.completedFuture(null))
        .setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null))
        .setOnPostToolUse((input, inv) -> CompletableFuture.completedFuture(null));
        // ... add only the hooks you need

    var session = client.createSession(
        new SessionConfig()
            .setHooks(hooks)
            .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
    ).get();
}

모든 후크 처리기는 세션별 상태를 유지하고 로그의 상관 관계를 지정하는 데 유용한 invocation을 포함하는 매개 변수 sessionId를 수신합니다.

사용 사례: 권한 제어

에이전트가 실행할 수 있는 도구, 허용되는 인수 및 실행 전에 사용자에게 메시지가 표시되어야 하는지 여부를 결정하는 권한 계층을 빌드하는 데 사용합니다 onPreToolUse .

"안전한 도구 집합을 허용 목록으로 등록"

TypeScript
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: "approve-once" }),
});
Python
from copilot import PermissionDecisionApproveOnce

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

async def on_pre_tool_use(input_data, invocation):
    if input_data["toolName"] not in READ_ONLY_TOOLS:
        return {
            "permissionDecision": "deny",
            "permissionDecisionReason":
                f'Only read-only tools are allowed. "{input_data["toolName"]}" was blocked.',
        }
    return {"permissionDecision": "allow"}

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={"on_pre_tool_use": on_pre_tool_use},
)
Go
package main

import (
    "context"
    "fmt"
    copilot "github.com/github/copilot-sdk/go"
    "github.com/github/copilot-sdk/go/rpc"
)

func main() {
    ctx := context.Background()
    client := copilot.NewClient(nil)

    readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}

    session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
        Hooks: &copilot.SessionHooks{
            OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
                if !readOnlyTools[input.ToolName] {
                    return &copilot.PreToolUseHookOutput{
                        PermissionDecision:       "deny",
                        PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
                    }, nil
                }
                return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
            },
        },
        OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
            return &rpc.PermissionDecisionApproveOnce{}, nil
        },
    })
    _ = session
}
readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}

session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
    Hooks: &copilot.SessionHooks{
        OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
            if !readOnlyTools[input.ToolName] {
                return &copilot.PreToolUseHookOutput{
                    PermissionDecision:       "deny",
                    PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
                }, nil
            }
            return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
        },
    },
})
.NET
using GitHub.Copilot;
using GitHub.Copilot.Rpc;

public static class PermissionControlExample
{
    public static async Task Main()
    {
        await using var client = new CopilotClient();

        var readOnlyTools = new HashSet<string> { "read_file", "glob", "grep", "view" };

        var session = await client.CreateSessionAsync(new SessionConfig
        {
            Hooks = new SessionHooks
            {
                OnPreToolUse = (input, invocation) =>
                {
                    if (!readOnlyTools.Contains(input.ToolName))
                    {
                        return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput
                        {
                            PermissionDecision = "deny",
                            PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
                        });
                    }
                    return Task.FromResult<PreToolUseHookOutput?>(
                        new PreToolUseHookOutput { PermissionDecision = "allow" });
                },
            },
            OnPermissionRequest = (req, inv) =>
                Task.FromResult(PermissionDecision.ApproveOnce()),
        });
    }
}
var readOnlyTools = new HashSet<string> { "read_file", "glob", "grep", "view" };

var session = await client.CreateSessionAsync(new SessionConfig
{
    Hooks = new SessionHooks
    {
        OnPreToolUse = (input, invocation) =>
        {
            if (!readOnlyTools.Contains(input.ToolName))
            {
                return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput
                {
                    PermissionDecision = "deny",
                    PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
                });
            }
            return Task.FromResult<PreToolUseHookOutput?>(
                new PreToolUseHookOutput { PermissionDecision = "allow" });
        },
    },
});
Java
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import com.github.copilot.sdk.PermissionHandler;
import com.github.copilot.sdk.SessionConfig;
import com.github.copilot.sdk.SessionHooks;
import com.github.copilot.sdk.json.PreToolUseHookOutput;
var readOnlyTools = Set.of("read_file", "glob", "grep", "view");

var hooks = new SessionHooks()
    .setOnPreToolUse((input, invocation) -> {
        if (!readOnlyTools.contains(input.getToolName())) {
            return CompletableFuture.completedFuture(
                PreToolUseHookOutput.deny(
                    "Only read-only tools are allowed. \"" + input.getToolName() + "\" was blocked.")
            );
        }
        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
    });

var session = client.createSession(
    new SessionConfig()
        .setHooks(hooks)
        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();

특정 디렉터리에 대한 파일 액세스 제한

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: "approve-once" }),
});

파괴적인 작업 전에 사용자에게 문의

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: "approve-once" }),
});

반환하면 "ask" 런타임에 사용자에게 결정을 위임합니다. 이는 사람이 관여해야 하는 파괴적인 작업에서 유용합니다.

사용 사례: 감사 및 규정 준수

onPreToolUse, onPostToolUse 및 세션 수명 주기 후크를 결합하여 에이전트가 수행하는 모든 작업을 기록하는 완전한 감사 추적을 작성합니다.

구조적 감사 로그

TypeScript
interface AuditEntry {
  timestamp: Date;
  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: "approve-once" }),
});
Python
import json, aiofiles
from copilot import PermissionDecisionApproveOnce

audit_log = []

async def on_session_start(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "session_start",
    })
    return None

async def on_user_prompt_submitted(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "user_prompt",
        "prompt": input_data["prompt"],
    })
    return None

async def on_pre_tool_use(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "tool_call",
        "tool_name": input_data["toolName"],
        "tool_args": input_data["toolArgs"],
    })
    return {"permissionDecision": "allow"}

async def on_post_tool_use(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "tool_result",
        "tool_name": input_data["toolName"],
        "tool_result": input_data["toolResult"],
    })
    return None

async def on_session_end(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "session_end",
    })
    async with aiofiles.open(f"audit-{invocation['session_id']}.json", "w") as f:
        await f.write(json.dumps(audit_log, indent=2))
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_user_prompt_submitted": on_user_prompt_submitted,
        "on_pre_tool_use": on_pre_tool_use,
        "on_post_tool_use": on_post_tool_use,
        "on_session_end": on_session_end,
    },
)

도구 결과에서 비밀 항목 제거

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: "approve-once" }),
});

사용 사례: 알림 및 소리

후크는 애플리케이션의 프로세스에서 실행되므로 데스크톱 알림, 소리, Slack 메시지 또는 웹후크 호출과 같은 후속 작업을 실행할 수 있습니다.

세션 이벤트에 대한 데스크톱 알림

TypeScript
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: "approve-once" }),
});
Python
import subprocess
from copilot import PermissionDecisionApproveOnce

async def on_session_end(input_data, invocation):
    sid = invocation["session_id"][:8]
    reason = input_data["reason"]
    subprocess.Popen([
        "notify-send", "Copilot Session Complete",
        f"Session {sid} finished ({reason}).",
    ])
    return None

async def on_error_occurred(input_data, invocation):
    subprocess.Popen([
        "notify-send", "Copilot Error",
        input_data["error"][:200],
    ])
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_end": on_session_end,
        "on_error_occurred": on_error_occurred,
    },
)

도구가 작업을 마치면 소리를 재생

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: "approve-once" }),
});

오류 발생 후 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: "approve-once" }),
});

사용 사례: 프롬프트 강화

onSessionStartonUserPromptSubmitted을 사용하여 자동으로 컨텍스트를 삽입하면 사용자가 스스로 반복할 필요가 없습니다.

세션 시작 시 프로젝트 메타데이터 삽입

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}`,
          `Working directory: ${input.workingDirectory}`,
        ].join("\n"),
      };
    },
  },
  onPermissionRequest: async () => ({ kind: "approve-once" }),
});

프롬프트에서 줄임 명령어를 확장하기

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: "approve-once" }),
});

사용 사례: 오류 처리 및 복구

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: "approve-once" }),
});

친숙한 오류 메시지

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: "approve-once" }),
});

사용 사례: 세션 메트릭

세션 실행 시간, 호출되는 도구 수 및 세션이 종료되는 이유를 추적하여 대시보드 및 비용 모니터링에 유용합니다.

TypeScript
const metrics = new Map<
  string,
  { start: Date; 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.getTime() - m.start.getTime()) / 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: "approve-once" }),
});
Python
from copilot import PermissionDecisionApproveOnce

session_metrics = {}

async def on_session_start(input_data, invocation):
    session_metrics[invocation["session_id"]] = {
        "start": input_data["timestamp"],
        "tool_calls": 0,
        "prompts": 0,
    }
    return None

async def on_user_prompt_submitted(input_data, invocation):
    session_metrics[invocation["session_id"]]["prompts"] += 1
    return None

async def on_pre_tool_use(input_data, invocation):
    session_metrics[invocation["session_id"]]["tool_calls"] += 1
    return {"permissionDecision": "allow"}

async def on_session_end(input_data, invocation):
    m = session_metrics.pop(invocation["session_id"])
    duration = (input_data["timestamp"] - m["start"]).total_seconds()
    sid = invocation["session_id"][:8]
    print(
        f"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, "
        f"{m['tool_calls']} tool calls, ended: {input_data['reason']}"
    )
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_user_prompt_submitted": on_user_prompt_submitted,
        "on_pre_tool_use": on_pre_tool_use,
        "on_session_end": on_session_end,
    },
)

후크 결합

후크는 자연스럽게 조합됩니다. 단일 hooks 개체는 사용 권한 감사 및 알림을 처리할 수 있습니다 . 각 후크는 자체 작업을 수행합니다.

const session = await client.createSession({
  hooks: {
    onSessionStart: async (input) => {
      console.log(`[audit] session started in ${input.workingDirectory}`);
      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: "approve-once" }),
});

모범 사례

  1. 후크가 빠르게 작동하도록 유지합니다. 모든 후크는 인라인으로 실행되며 느린 후크는 대화를 지연합니다. 가능하면 백그라운드 큐에 많은 작업(데이터베이스 쓰기, HTTP 호출)을 오프로드합니다.

  2. 변경할 내용이 없으면 반환 null 합니다. 이렇게 하면 SDK에서 기본값을 계속 진행하도록 지시하고 불필요한 개체 할당을 방지합니다.

  3. 사용 권한 결정에 명시적이어야 합니다. 도구를 허용하더라도 { permissionDecision: "allow" } 반환은 null 반환보다 더 명확합니다.

  4. 중요한 오류를 삼켜도 안됩니다. 복구 가능한 도구 오류를 표시하지 않는 것은 괜찮지만, 복구할 수 없는 오류에 대해 항상 로그 또는 경고를 표시합니다.

  5. 가능한 경우 대신 additionalContext 사용합니다modifiedPrompt. 컨텍스트를 추가하면 모델을 계속 안내하는 동안 사용자의 원래 의도가 유지됩니다.

  6. 세션 ID별 범위 상태입니다. 세션별 데이터를 추적하는 경우 invocation.sessionId에 키를 설정하고 onSessionEnd에서 정리합니다.

Reference

전체 형식 정의, 입력/출력 필드 테이블 및 모든 후크에 대한 추가 예제는 API 참조를 참조하세요.

참고하십시오