research

pi SDK — Extension API 상세

pi SDK — Extension API 상세

pi-mono의 Extension 시스템 완전 분석. 목적: 일정 관리 프로그램에서 Python으로 동일 패턴 구현 시 레퍼런스.


Extension이란

Extensions는 lifecycle 이벤트를 구독하고, LLM이 호출할 수 있는 커스텀 도구를 등록하고, 커맨드를 추가할 수 있는 TypeScript 모듈이다.

// 기본 구조
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";

export default function(pi: ExtensionAPI) {
  pi.on("이벤트명", async (event, ctx) => { ... });
  pi.registerTool({ ... });
  pi.registerCommand("cmd", { ... });
}

로딩 위치:

  • ~/.pi/agent/extensions/ — 전역 (모든 프로젝트)
  • .pi/extensions/ — 프로젝트 로컬 (우선순위 높음)
  • pi -e ./path.ts — 테스트용 즉시 로드
  • jiti로 로드되므로 TypeScript 컴파일 없이 실행

전체 이벤트 흐름

pi 시작 → session_directory → session_start 사용자 프롬프트 → input → before_agent_start → agent_start turn_start → context → before_provider_request (LLM 응답) tool_execution_start → tool_call → tool_execution_update → tool_result → tool_execution_end turn_end agent_end


이벤트 목록

세션 lifecycle

| 이벤트 | 시점 | 반환값 | | ----------------------- | ----------------- | -------------------- | | session_directory | CLI 시작 시 (ctx 없음) | - | | session_start | 세션 시작 | - | | session_shutdown | 종료 직전 | - | | session_tree | 세션 트리 변경 | - | | session_before_switch | 세션 전환 전 | cancel: true 로 차단 가능 |

Agent lifecycle

| 이벤트 | 시점 | 반환값/활용 | | -------------------- | ------------ | ----------------------- | | input | 사용자 입력 수신 | 변환/처리/차단 가능 | | before_agent_start | 에이전트 루프 시작 전 | 메시지 inject, 시스템 프롬프트 수정 | | agent_start | 에이전트 루프 시작 | - | | agent_end | 에이전트 루프 종료 | - |

Turn lifecycle (LLM 한 번 호출 단위)

| 이벤트 | 시점 | 반환값/활용 | |--------|------|------------| | turn_start | LLM 호출 직전 | - | | context | 메시지 컨텍스트 전달 전 | 메시지 수정 가능 | | before_provider_request | API 요청 직전 | payload 검사/교체 | | turn_end | LLM 응답 완료 | - |

Tool execution

| 이벤트 | 시점 | 반환값/활용 | |--------|------|------------| | tool_execution_start | 도구 실행 시작 | - | | tool_call | 도구 호출 직전 | { block: true, reason }으로 차단 | | tool_execution_update | 실행 중 스트리밍 업데이트 | - | | tool_result | 도구 결과 반환 후 | 결과 수정 가능 | | tool_execution_end | 실행 완료 | - | | user_bash | 사용자가 직접 bash 실행 | 차단 가능 |

Message streaming

| 이벤트 | 시점 | |--------|------| | message_start | 메시지 시작 | | message_update | 스트리밍 업데이트 | | message_end | 메시지 완료 |

Compaction

| 이벤트 | 시점 | 반환값/활용 | |--------|------|------------| | session_before_compact | 컴팩션 직전 | cancel 또는 커스텀 summary 제공 |


tool_call 이벤트 — 핵심 인터셉터

가장 강력한 이벤트. 도구 실행 전에 차단하거나 조건 검사 가능.

pi.on("tool_call", async (event, ctx) => {
  // 내장 도구 타입 가드
  if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
    const ok = await ctx.ui.confirm("위험!", "rm -rf 허용?");
    if (!ok) return { block: true, reason: "사용자 차단" };
  }

  // 커스텀 도구도 동일하게 처리
  if (event.toolName === "add_schedule") {
    // 일정 추가 전 검증 로직
  }
});

내장 도구 타입 가드:

isToolCallEventType("bash", event)   // BashToolCallEvent
isToolCallEventType("read", event)   // ReadToolCallEvent
isToolCallEventType("edit", event)   // EditToolCallEvent
isToolCallEventType("write", event)  // WriteToolCallEvent
isToolCallEventType("grep", event)   // GrepToolCallEvent

registerTool — 커스텀 도구 등록

LLM이 직접 호출할 수 있는 도구를 등록.

import { Type } from "@sinclair/typebox";

pi.registerTool({
  name: "add_schedule",
  label: "일정 추가",
  description: "새 일정을 등록한다",

  // TypeBox 스키마로 파라미터 정의
  parameters: Type.Object({
    title: Type.String({ description: "일정 제목" }),
    due_date: Type.String({ description: "마감일 (YYYY-MM-DD)" }),
    priority: Type.Optional(Type.Number({ description: "우선순위 1-5" })),
  }),

  // 도구 실행
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // onUpdate: 스트리밍 중간 결과 전송
    onUpdate({ type: "text", text: "처리 중..." });

    // signal: AbortSignal (취소 감지)
    if (signal.aborted) throw new Error("취소됨");

    const result = await db.addSchedule(params);
    return {
      content: [{ type: "text", text: `등록됨: ${result.id}` }],
      details: { id: result.id },  // 렌더링용 추가 정보
    };
  },

  // 시스템 프롬프트에 노출될 한 줄 설명 (선택)
  snippet: "add_schedule(title, due_date) - 일정 DB에 추가",

  // 시스템 프롬프트 Guidelines에 추가될 항목 (선택)
  guidelines: ["날짜는 반드시 YYYY-MM-DD 형식으로"],
});

도구 출력 제한: 50KB / 2000줄 초과 시 자동 잘림. 큰 출력은 반드시 요약해서 반환할 것.


before_agent_start — 메시지/프롬프트 주입

에이전트 루프 시작 전에 컨텍스트를 추가할 수 있는 핵심 포인트.

pi.on("before_agent_start", async (event, ctx) => {
  // 시스템 프롬프트 확인
  const prompt = ctx.getSystemPrompt();

  // 메시지 inject (context-bridge 역할)
  ctx.inject({
    role: "user",
    content: "현재 대기 중인 일정: [...]"
  });

  // 시스템 프롬프트 수정
  ctx.modifySystemPrompt("오늘 날짜: 2026-03-22");
});

context 이벤트 — 메시지 수정

LLM에 전달되기 직전 메시지를 수정.

pi.on("context", async (event, ctx) => {
  // event.messages: LLM에 전달될 메시지 배열
  // 수정하거나 필터링 가능
  return {
    messages: event.messages.filter(m => !m.content.includes("민감한 정보"))
  };
});

session_before_compact — 컴팩션 커스터마이징

로컬 LLM으로 요약 생성 시 활용.

pi.on("session_before_compact", async (event, ctx) => {
  const { preparation, signal } = event;

  // 로컬 LLM으로 직접 요약 생성
  const summary = await localLLM.summarize(
    preparation.messagesToSummarize,
    { signal }
  );

  return {
    compaction: {
      summary,
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      details: {}
    }
  };

  // 취소: return { cancel: true }
});

ExtensionContext — 이벤트 핸들러에서 사용 가능한 ctx

ctx.ui.notify("메시지", "info" | "success" | "error" | "warning")
ctx.ui.confirm("제목", "내용")  // boolean 반환
ctx.ui.select("제목", ["옵션1", "옵션2"])  // 선택된 값 반환
ctx.ui.input("제목", "placeholder")  // 문자열 반환
ctx.ui.setStatus("my-ext", "처리 중...")  // 하단 상태바
ctx.ui.setWidget("my-ext", ["줄1", "줄2"])  // 에디터 위 위젯

ctx.getContextUsage()  // { tokens, limit } 컨텍스트 사용량
ctx.getSystemPrompt()  // 현재 시스템 프롬프트
ctx.compact({ customInstructions: "최근 변경사항 중심으로" })  // 수동 컴팩션
ctx.shutdown()  // pi 종료 요청

ctx.sessionManager  // 세션 JSONL 직접 접근
ctx.modelRegistry   // 모델 목록
ctx.authStorage     // 인증 정보

ExtensionCommandContext — 커맨드에서 추가로 사용 가능

// 커맨드 핸들러에서만 사용 가능 (이벤트 핸들러에서 쓰면 데드락)
await ctx.waitForIdle()   // 에이전트가 idle 상태가 될 때까지 대기
await ctx.newSession({    // 새 세션 생성
  parentSession: ...,
  setup: async (sm) => { sm.appendMessage(...) }
})
ctx.reload()              // extension/skill/prompt 재로드

registerCommand / registerShortcut

pi.registerCommand("sync-schedule", {
  description: "일정 DB와 동기화",
  handler: async (args, ctx) => {
    await ctx.waitForIdle();
    const count = await syncSchedules();
    ctx.ui.notify(`${count}개 동기화 완료`, "success");
  }
});

pi.registerShortcut("ctrl+s", {
  description: "빠른 저장",
  handler: async (ctx) => { ... }
});

세션 영속화 — appendEntry

// Extension 상태를 세션 파일에 저장 (재시작 후에도 복원)
pi.appendEntry({
  type: "extension_state",
  data: { lastSync: Date.now(), pendingTasks: [...] }
});

Python으로 구현할 때 대응 관계

| pi Extension | Python 구현 | |-------------|-------------| | pi.on("tool_call") | async def on_tool_call(event) callback | | pi.registerTool() | @agent.tool 데코레이터 또는 tool dict 등록 | | pi.on("before_agent_start") | async def inject_context() pre-hook | | ctx.ui.confirm() | asyncio Queue로 사용자 입력 대기 | | ctx.sessionManager | JSONL 파일 직접 read/append | | ctx.compact() | Local LLM 요약 후 메시지 교체 | | Extension 로딩 | importlib.import_module() |


내 일정 프로그램에 적용할 핵심 패턴

# Python pseudo-code

class ScheduleExtension:
    def on_before_agent_start(self, ctx):
        # 오늘의 일정 컨텍스트 inject
        pending = db.get_pending_tasks()
        ctx.inject_message(f"현재 대기 중인 작업: {pending}")

    def on_tool_call(self, event, ctx):
        # VLM 작업 감지 → VRAM 스왑 처리
        if event.tool_name == "run_vlm":
            if not vram_manager.can_load_vlm():
                vram_manager.unload_current()
            # 허용

    def register_tools(self):
        return [
            Tool("add_schedule", self.add_schedule),
            Tool("fetch_assignments", self.fetch_assignments),
            Tool("run_vlm_conversion", self.run_vlm),
        ]

    async def add_schedule(self, params):
        result = await db.add(params)
        return f"등록됨: {result.id}"

관련