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}"
관련
- overview — pi SDK 전체 분석
- llm-vram-model-swap — VRAM 스왑 전략
- pipeline-overview — LLM agent 전체 파이프라인