Extensions
pi는 확장을 만들 수 있습니다. 귀하의 사용 사례에 맞게 구축하도록 요청하세요.
Extensions는 pi의 동작을 확장하는 TypeScript 모듈입니다. 수명 주기 이벤트를 구독하고, LLM에서 호출할 수 있는 사용자 정의 도구를 등록하고, 명령을 추가하는 등의 작업을 수행할 수 있습니다.
/reload 배치: 자동 검색을 위해
~/.pi/agent/extensions/(전역) 또는.pi/extensions/(프로젝트-로컬)에 확장을 넣습니다. 빠른 테스트에만pi -e./path.ts를 사용하세요. Extensions 자동 검색된 위치는/reload로 핫 리로드될 수 있습니다.
주요 기능:
- 사용자 정의 도구 - LLM이
pi.registerTool()를 통해 호출할 수 있는 도구를 등록합니다. - 이벤트 차단 - 도구 호출 차단 또는 수정, 컨텍스트 삽입, 압축 사용자 정의
- 사용자 상호작용 -
ctx.ui을 통해 사용자에게 메시지 표시(선택, 확인, 입력, 알림) - 사용자 정의 UI 구성 요소 - 복잡한 상호 작용을 위해
ctx.ui.custom()를 통한 키보드 입력이 포함된 전체 TUI 구성 요소 - 사용자 정의 명령 -
pi.registerCommand()을 통해/mycommand와 같은 명령을 등록합니다. - 세션 지속성 -
pi.appendEntry()을 통해 다시 시작해도 유지되는 상태 저장 - 맞춤 렌더링 - 도구 호출/결과 및 메시지가 TUI에 표시되는 방식 제어
사용 사례 예시:
- 허가 게이트(
rm -rf,sudo등 이전에 확인) - Git 체크포인트(매 턴마다 보관, 분기에 복원)
- 경로 보호(
.env,node_modules/에 대한 쓰기 차단) - 사용자 정의 압축(대화를 원하는 방식으로 요약)
- 대화 요약(
summarize.ts예 참조) - 대화형 도구(질문, 마법사, 사용자 정의 대화 상자)
- 상태 저장 도구(할 일 목록, 연결 풀)
- 외부 통합(파일 감시자, 웹후크, CI 트리거)
- 기다리는 동안 게임을 즐기세요(
snake.ts예시 참조)
작업 구현은 examples/extensions/를 참조하세요.
목차
- Quick Start
- Extension Locations
- Available Imports
- Writing an Extension
- Events
- ExtensionContext
- ExtensionCommandContext
- ExtensionAPI Methods
- State Management
- Custom Tools
- Custom UI
- Error Handling
- Mode Behavior
- Examples Reference
빠른 시작
~/.pi/agent/extensions/my-extension.ts 만들기:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
// React to events
pi.on("session_start", async (_event, ctx) => {
ctx.ui.notify("Extension loaded!", "info");
});
pi.on("tool_call", async (event, ctx) => {
if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
if (!ok) return { block: true, reason: "Blocked by user" };
}
});
// Register a custom tool
pi.registerTool({
name: "greet",
label: "Greet",
description: "Greet someone by name",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
return {
content: [{ type: "text", text: `Hello, ${params.name}!` }],
details: {},
};
},
});
// Register a command
pi.registerCommand("hello", {
description: "Say hello",
handler: async (args, ctx) => {
ctx.ui.notify(`Hello ${args || "world"}!`, "info");
},
});
}--extension(또는 -e) 플래그로 테스트합니다.
pi -e ./my-extension.ts확장 위치
보안: Extensions 전체 시스템 권한으로 실행하고 임의 코드를 실행할 수 있습니다. 신뢰할 수 있는 소스에서만 설치하세요.
Extensions은 신뢰할 수 있는 위치에서 자동으로 검색됩니다. Project-local .pi/extensions 항목은 프로젝트를 신뢰한 후에만 로드됩니다.
| 위치 | 범위 |
|---|---|
~/.pi/agent/extensions/*.ts |
글로벌(모든 프로젝트) |
~/.pi/agent/extensions/*/index.ts |
전역(하위 디렉터리) |
.pi/extensions/*.ts |
프로젝트-로컬 |
.pi/extensions/*/index.ts |
프로젝트-로컬(하위 디렉터리) |
settings.json를 통한 추가 경로:
{
"packages": [
"npm:@foo/bar@1.0.0",
"git:github.com/user/repo@v1"
],
"extensions": [
"/path/to/local/extension.ts",
"/path/to/local/extension/dir"
]
}npm 또는 git을 pi 패키지로 공유하려면 packages.md를 참조하세요.
사용 가능한 수입품
| 패키지 | 목적 |
|---|---|
@earendil-works/pi-coding-agent |
확장 유형(ExtensionAPI, ExtensionContext, 이벤트) |
typebox |
도구 매개변수에 대한 스키마 정의 |
@earendil-works/pi-ai |
AI 유틸리티(Google 호환 열거형의 경우 StringEnum) |
@earendil-works/pi-tui |
TUI 맞춤 렌더링을 위한 구성요소 |
npm 종속성도 작동합니다. 확장 프로그램 옆(또는 상위 디렉토리)에 package.json를 추가하고 npm install를 실행하면 node_modules/에서의 가져오기가 자동으로 해결됩니다.
pi install(npm 또는 git)로 설치된 분산 pi 패키지의 경우 런타임 deps는 dependencies에 있어야 합니다. 패키지 설치는 기본적으로 프로덕션 설치(npm install --omit=dev)를 사용하므로 런타임 시 devDependencies를 사용할 수 없습니다. npmCommand가 구성되면 git 패키지는 래퍼와의 호환성을 위해 일반 install를 사용합니다.
Node.js 내장 기능(node:fs, node:path 등)도 사용할 수 있습니다.
확장 작성
확장은 ExtensionAPI를 수신하는 기본 팩토리 함수를 내보냅니다. 팩토리는 동기식 또는 비동기식일 수 있습니다.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default function (pi: ExtensionAPI) {
// Subscribe to events
pi.on("event_name", async (event, ctx) => {
// ctx.ui for user interaction
const ok = await ctx.ui.confirm("Title", "Are you sure?");
ctx.ui.notify("Done!", "info");
ctx.ui.setStatus("my-ext", "Processing..."); // Footer status
ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]); // Widget above editor (default)
});
// Register tools, commands, shortcuts, flags
pi.registerTool({ ... });
pi.registerCommand("name", { ... });
pi.registerShortcut("ctrl+x", { ... });
pi.registerFlag("my-flag", { ... });
}Extensions은 jiti를 통해 로드되므로 TypeScript는 컴파일 없이 작동합니다.
공장에서 Promise를 반환하면 pi는 시작을 계속하기 전에 이를 기다립니다. 이는 session_start 이전, resources_discover 이전 및 pi.registerProvider()을 통해 대기열에 있는 공급자 등록이 플러시되기 전에 비동기 초기화가 완료됨을 의미합니다.
비동기 팩토리 기능
원격 구성을 가져오거나 사용 가능한 모델을 동적으로 검색하는 등의 일회성 시작 작업에 비동기 팩토리를 사용하세요.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
export default async function (pi: ExtensionAPI) {
const response = await fetch("http://localhost:1234/v1/models");
const payload = (await response.json()) as {
data: Array<{
id: string;
name?: string;
context_window?: number;
max_tokens?: number;
}>;
};
pi.registerProvider("local-openai", {
baseUrl: "http://localhost:1234/v1",
apiKey: "$LOCAL_OPENAI_API_KEY",
api: "openai-completions",
models: payload.data.map((model) => ({
id: model.id,
name: model.name ?? model.id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: model.context_window ?? 128000,
maxTokens: model.max_tokens ?? 4096,
})),
});
}이 패턴을 사용하면 일반 시작 및 pi --list-models에서 가져온 모델을 사용할 수 있습니다.
수명이 긴 리소스 및 종료
확장 팩토리는 세션을 시작하지 않는 호출로 실행될 수 있습니다. 공장에서 프로세스, 소켓, 파일 감시자 또는 타이머와 같은 백그라운드 리소스를 시작하지 마십시오.
session_start 또는 리소스가 필요한 명령/도구/이벤트가 발생할 때까지 백그라운드 리소스 시작을 연기합니다. 시작한 모든 세션 범위 리소스를 닫으려면 멱등성 session_shutdown 핸들러를 등록하세요.
확장 스타일
단일 파일 - 소규모 확장의 경우 가장 간단함:
~/.pi/agent/extensions/
└── my-extension.tsindex.ts가 있는 디렉터리 - 다중 파일 확장자의 경우:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper module종속성이 있는 패키지 - npm 패키지가 필요한 확장 프로그램의 경우:
~/.pi/agent/extensions/
└── my-extension/
├── package.json # Declares dependencies and entry points
├── package-lock.json
├── node_modules/ # After npm install
└── src/
└── index.ts// package.json
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}확장 디렉토리에서 npm install를 실행하면 node_modules/에서 가져오기가 자동으로 작동합니다.
이벤트
수명주기 개요
pi starts
│
├─► project_trust (user/global and CLI extensions only, before project resources load)
├─► session_start { reason: "startup" }
└─► resources_discover { reason: "startup" }
│
▼
user sends prompt ─────────────────────────────────────────┐
│ │
├─► (extension commands checked first, bypass if found) │
├─► input (can intercept, transform, or handle) │
├─► (skill/template expansion if not handled) │
├─► before_agent_start (can inject message, modify system prompt)
├─► agent_start │
├─► message_start / message_update / message_end │
│ │
│ ┌─── turn (repeats while LLM calls tools) ───┐ │
│ │ │ │
│ ├─► turn_start │ │
│ ├─► context (can modify messages) │ │
│ ├─► before_provider_headers (can mutate headers) |
│ ├─► before_provider_request (can inspect or replace payload)
│ ├─► after_provider_response (status + headers, before stream consume)
│ │ │ │
│ │ LLM responds, may call tools: │ │
│ │ ├─► tool_execution_start │ │
│ │ ├─► tool_call (can block) │ │
│ │ ├─► tool_execution_update │ │
│ │ ├─► tool_result (can modify) │ │
│ │ └─► tool_execution_end │ │
│ │ │ │
│ └─► turn_end │ │
│ │
├─► agent_end │
└─► agent_settled (no retry/compaction/follow-up left) │
│
user sends another prompt ◄────────────────────────────────┘
/new (new session) or /resume (switch session)
├─► session_before_switch (can cancel)
├─► session_shutdown
├─► session_start { reason: "new" | "resume", previousSessionFile? }
└─► resources_discover { reason: "startup" }
/fork or /clone
├─► session_before_fork (can cancel)
├─► session_shutdown
├─► session_start { reason: "fork", previousSessionFile }
└─► resources_discover { reason: "startup" }
/name or pi.setSessionName()
└─► session_info_changed
/compact or auto-compaction
├─► session_before_compact (can cancel or customize)
└─► session_compact
/tree navigation
├─► session_before_tree (can cancel or customize)
└─► session_tree
/model or Ctrl+P (model selection/cycling)
├─► thinking_level_select (if model change changes/clamps thinking level)
└─► model_select
thinking level changes (settings, keybinding, pi.setThinkingLevel())
└─► thinking_level_select
exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
└─► session_shutdown스타트업 이벤트
프로젝트_신뢰
pi가 동적 구성(.pi 또는 .agents/skills)이 포함된 프로젝트를 신뢰할지 여부를 결정하기 전에 실행됩니다. 시작하는 동안 그리고 세션 교체(예: /resume)가 현재 프로세스에서 신뢰가 확인되지 않은 cwd에 들어갈 때 실행됩니다. 사용자/전역 확장 및 CLI -e 확장만 참여합니다. 프로젝트 로컬 확장은 신뢰가 해결될 때까지 로드되지 않습니다.
pi.on("project_trust", async (event, ctx) => {
// event.cwd - current working directory
// ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
if (await ctx.ui.confirm("Trust project?", event.cwd)) {
return { trusted: "yes", remember: true };
}
return { trusted: "undecided" };
});project_trust 핸들러는 { trusted: "yes" | "no" | "undecided" }를 반환해야 합니다. "yes" 또는 "no"를 반환하는 사용자/전역 또는 CLI 확장이 결정을 소유합니다. 첫 번째 예/아니요 결정이 승리하고 기본 제공 신뢰 프롬프트가 표시되지 않습니다. 예/아니요 결정을 유지하려면 remember: true를 사용하세요. 그렇지 않으면 현재 프로세스에만 적용됩니다. 이후 핸들러나 내장된 신뢰 흐름이 결정하도록 하려면 "undecided"를 반환합니다. 메시지를 표시하기 전에 ctx.hasUI를 확인하세요. 핸들러가 예/아니요를 반환하지 않으면 일반적인 신뢰 해결이 계속됩니다. 저장된 trust.json 결정이 먼저 적용된 다음 defaultProjectTrust는 기본적으로 pi의 요청, 신뢰 또는 거부 여부를 제어합니다.
자원 이벤트
리소스_발견
확장 프로그램이 추가 기술, 프롬프트 및 테마 경로에 기여할 수 있도록 session_start 이후에 실행됩니다.
시작 경로는 reason: "startup"를 사용합니다. 다시 로드는 reason: "reload"를 사용합니다.
pi.on("resources_discover", async (event, _ctx) => {
// event.cwd - current working directory
// event.reason - "startup" | "reload"
return {
skillPaths: ["/path/to/skills"],
promptPaths: ["/path/to/prompts"],
themePaths: ["/path/to/themes"],
};
});세션 이벤트
세션 저장소 내부 및 SessionManager API에 대해서는 Session Format를 참조하세요.
세션_시작
세션이 시작, 로드 또는 다시 로드될 때 발생합니다.
pi.on("session_start", async (event, ctx) => {
// event.reason - "startup" | "reload" | "new" | "resume" | "fork"
// event.previousSessionFile - present for "new", "resume", and "fork"
ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});세션_정보_변경됨
현재 세션 표시 이름이 /name, RPC 또는 pi.setSessionName()를 통해 설정되면 시작됩니다.
pi.on("session_info_changed", async (event, ctx) => {
// event.name - current normalized name, or undefined if cleared
ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});session_before_switch
새 세션을 시작(/new)하거나 세션을 전환(/resume)하기 전에 실행됩니다.
pi.on("session_before_switch", async (event, ctx) => {
// event.reason - "new" or "resume"
// event.targetSessionFile - session we're switching to (only for "resume")
if (event.reason === "new") {
const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
if (!ok) return { cancel: true };
}
});성공적인 전환 또는 새 세션 작업 후 pi는 이전 확장 인스턴스에 대해 session_shutdown를 내보내고 새 세션에 대한 확장을 다시 로드하고 리바인드한 다음 reason: "new" | "resume" 및 previousSessionFile와 함께 session_start을 내보냅니다.
session_shutdown에서 정리 작업을 수행한 다음 session_start에서 메모리 내 상태를 다시 설정합니다.
session_before_fork
/fork를 통해 분기하거나 /clone를 통해 복제할 때 실행됩니다.
pi.on("session_before_fork", async (event, ctx) => {
// event.entryId - ID of the selected entry
// event.position - "before" for /fork, "at" for /clone
return { cancel: true }; // Cancel fork/clone
// OR
return { skipConversationRestore: true }; // Reserved for future conversation restore control
});성공적인 포크 또는 복제 후 pi는 이전 확장 인스턴스에 대해 session_shutdown를 내보내고 새 세션에 대한 확장을 다시 로드하고 리바인드한 다음 reason: "fork" 및 previousSessionFile를 사용하여 session_start을 내보냅니다.
session_shutdown에서 정리 작업을 수행한 다음 session_start에서 메모리 내 상태를 다시 설정합니다.
session_before_compact / 세션_콤팩트
압축시 발사됩니다. 자세한 내용은 compaction.md를 참조하세요.
pi.on("session_before_compact", async (event, ctx) => {
const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;
// reason - "manual" (/compact), "threshold", or "overflow"
// willRetry - whether the aborted turn is retried after compaction (overflow recovery)
// Cancel:
return { cancel: true };
// Custom summary:
return {
compaction: {
summary: "...",
firstKeptEntryId: preparation.firstKeptEntryId,
tokensBefore: preparation.tokensBefore,
// usage: summaryResponse.usage, // Optional; included in session totals
}
};
});
pi.on("session_compact", async (event, ctx) => {
// event.compactionEntry - the saved compaction
// event.fromExtension - whether extension provided it
// event.reason - "manual" (/compact), "threshold", or "overflow"
// event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});session_before_tree / 세션_트리
/tree 탐색에서 실행됩니다. 트리 탐색 개념은 Sessions를 참조하세요.
pi.on("session_before_tree", async (event, ctx) => {
const { preparation, signal } = event;
return { cancel: true };
// OR provide custom summary:
return {
summary: {
summary: "...",
// usage: summaryResponse.usage, // Optional; included in session totals
details: {},
},
};
});
pi.on("session_tree", async (event, ctx) => {
// event.newLeafId, oldLeafId, summaryEntry, fromExtension
});세션 종료
시작된 세션 런타임이 종료되기 전에 실행됩니다. session_start 또는 기타 세션 범위 후크에서 열린 리소스를 정리하려면 이를 사용합니다.
pi.on("session_shutdown", async (event, ctx) => {
// event.reason - "quit" | "reload" | "new" | "resume" | "fork"
// event.targetSessionFile - destination session for session replacement flows
// Cleanup, save state, etc.
});에이전트 이벤트
before_agent_start
사용자가 프롬프트를 제출한 후 에이전트 루프 전에 실행됩니다. 메시지를 삽입하거나 시스템 프롬프트를 수정할 수 있습니다.
pi.on("before_agent_start", async (event, ctx) => {
// event.prompt - user's prompt text
// event.images - attached images (if any)
// event.systemPrompt - current chained system prompt for this handler
// (includes changes from earlier before_agent_start handlers)
// event.systemPromptOptions - structured options used to build the system prompt
// .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
// .selectedTools - tools currently active in the prompt
// .toolSnippets - one-line descriptions for each tool
// .promptGuidelines - custom guideline bullets
// .appendSystemPrompt - text from --append-system-prompt flags
// .cwd - working directory
// .contextFiles - AGENTS.md files and other loaded context files
// .skills - loaded skills
return {
// Inject a persistent message (stored in session, sent to LLM)
message: {
customType: "my-extension",
content: "Additional context for the LLM",
display: true,
},
// Replace the system prompt for this turn (chained across extensions)
systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
};
});systemPromptOptions 필드는 Pi가 시스템 프롬프트를 구축하는 데 사용하는 것과 동일한 구조화된 데이터에 대한 액세스를 확장 프로그램에 제공합니다. 이를 통해 리소스를 다시 검색하거나 플래그를 다시 구문 분석하지 않고도 Pi가 로드한 내용(사용자 정의 프롬프트, 지침, 도구 조각, context files, 기술)을 검사할 수 있습니다. 확장 프로그램이 사용자가 제공한 구성을 존중하면서 시스템 프롬프트에 대해 심층적이고 정보에 입각한 변경을 수행해야 하는 경우 이를 사용하세요.
before_agent_start, event.systemPrompt 및 ctx.getSystemPrompt() 내부에는 현재 핸들러의 연결된 시스템 프롬프트가 반영됩니다. 나중에 before_agent_start 핸들러가 이를 다시 수정할 수 있습니다.
에이전트_시작/에이전트_끝/에이전트_정착
agent_start 낮은 수준의 에이전트 실행이 시작되면 실행됩니다. agent_end는 실행이 끝나면 실행되지만 Pi는 여전히 자동 재시도, 자동 압축 및 재시도하거나 대기 중인 후속 메시지를 계속 사용할 수 있습니다. Pi가 자동으로 계속 실행되지 않는다는 것을 알아야 하는 상태 통합의 경우 agent_settled를 사용하세요.
pi.on("agent_start", async (_event, ctx) => {});
pi.on("agent_end", async (event, ctx) => {
// event.messages - messages from this low-level run
});
pi.on("agent_settled", async (_event, ctx) => {
// ctx.isIdle() is true here unless another extension started a new run.
});턴_시작 / 턴_엔드
매 턴마다 실행됩니다(1개의 LLM 응답 + 도구 호출).
pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});message_start / message_update / message_end
메시지 수명 주기 업데이트를 위해 시작됩니다.
message_start및message_end사용자, 보조자 및 도구 결과 메시지에 대해 실행됩니다.message_update어시스턴트 스트리밍 업데이트를 위해 실행됩니다.message_end핸들러는{ message }를 반환하여 최종 메시지를 대체할 수 있습니다. 교체는 동일하게 유지되어야 합니다role.
pi.on("message_start", async (event, ctx) => {
// event.message
});
pi.on("message_update", async (event, ctx) => {
// event.message
// event.assistantMessageEvent (token-by-token stream event)
});
pi.on("message_end", async (event, ctx) => {
if (event.message.role !== "assistant") return;
return {
message: {
...event.message,
usage: {
...event.message.usage,
cost: {
...event.message.usage.cost,
total: 0.123,
},
},
},
};
});tool_execution_start / tool_execution_update / tool_execution_end
도구 실행 수명 주기 업데이트를 위해 시작됩니다.
병렬 도구 모드에서:
tool_execution_start는 비행 전 단계에서 보조 소스 순서로 방출됩니다.tool_execution_update이벤트가 여러 도구에 걸쳐 인터리브될 수 있음tool_execution_end는 각 도구가 완료된 후 도구 완료 순서대로 내보내집니다.- final
toolResult메시지 이벤트는 나중에 어시스턴트 소스 순서대로 방출됩니다.
pi.on("tool_execution_start", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args
});
pi.on("tool_execution_update", async (event, ctx) => {
// event.toolCallId, event.toolName, event.args, event.partialResult
});
pi.on("tool_execution_end", async (event, ctx) => {
// event.toolCallId, event.toolName, event.result, event.isError
});문맥
각 LLM 호출 전에 실행됩니다. 메시지를 비파괴적으로 수정합니다. 메시지 유형은 Session Format를 참조하세요.
pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});before_provider_headers
나가는 HTTP 헤더가 조합된 후에 실행됩니다. 요청 헤더를 추가, 재정의 또는 제거하는 데 사용합니다.
핸들러는 event.headers를 제자리에서 변경합니다. 추가하거나 재정의하려면 키를 문자열로 설정하고, 삭제하려면 null로 설정하세요.
pi.on("before_provider_headers", (event, ctx) => {
// Add or override — e.g. a session id for gateway tracing/attribution
event.headers["x-session-id"] = ctx.sessionManager.getSessionId();
// Drop a tracking header pi adds for this call
event.headers["X-OpenRouter-Title"] = null;
});공급자 요청당 한 번 실행됩니다. 재시도는 후크를 다시 실행하는 대신 동일한 헤더를 재사용합니다.
before_provider_request
공급자별 페이로드가 빌드된 후 요청이 전송되기 직전에 실행됩니다. 처리기는 확장 로드 순서로 실행됩니다. undefined를 반환하면 페이로드가 변경되지 않은 상태로 유지됩니다. 다른 값을 반환하면 이후 처리기와 실제 요청에 대한 페이로드가 대체됩니다.
이 후크는 공급자 수준 시스템 지침을 다시 작성하거나 완전히 제거할 수 있습니다. 이러한 페이로드 수준 변경 사항은 최종 직렬화된 공급자 페이로드가 아닌 Pi의 시스템 프롬프트 문자열을 보고하는 ctx.getSystemPrompt()에 반영되지 않습니다.
pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});이는 주로 공급자 직렬화 및 캐시 동작을 디버깅하는 데 유용합니다.
after_provider_response
HTTP 응답이 수신된 후 스트림 본문이 사용되기 전에 실행됩니다. 처리기는 확장 로드 순서로 실행됩니다.
pi.on("after_provider_response", (event, ctx) => {
// event.status - HTTP status code
// event.headers - normalized response headers
if (event.status === 429) {
console.log("rate limited", event.headers["retry-after"]);
}
});헤더 가용성은 공급자와 전송에 따라 다릅니다. Providers 추상 HTTP 응답은 헤더를 노출하지 않을 수 있습니다.
모델 이벤트
모델_선택
/model 명령, 모델 순환(Ctrl+P) 또는 세션 복원을 통해 모델이 변경되면 실행됩니다.
pi.on("model_select", async (event, ctx) => {
// event.model - newly selected model
// event.previousModel - previous model (undefined if first selection)
// event.source - "set" | "cycle" | "restore"
const prev = event.previousModel
? `${event.previousModel.provider}/${event.previousModel.id}`
: "none";
const next = `${event.model.provider}/${event.model.id}`;
ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});이를 사용하여 UI 요소(상태 표시줄, 바닥글)를 업데이트하거나 활성 모델이 변경될 때 모델별 초기화를 수행합니다.
Thinking_level_select
사고 수준이 바뀔 때 실행됩니다. 이는 알림 전용입니다. 핸들러 반환 값은 무시됩니다.
pi.on("thinking_level_select", async (event, ctx) => {
// event.level - newly selected thinking level
// event.previousLevel - previous thinking level
ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});pi.setThinkingLevel(), 모델 변경 또는 내장된 사고 수준 컨트롤이 활성 사고 수준을 변경할 때 이를 사용하여 확장 UI를 업데이트합니다.
도구 이벤트
도구 호출
도구가 실행되기 전인 tool_execution_start 이후에 실행됩니다. 차단 가능. 입력 범위를 좁히고 입력하려면 isToolCallEventType를 사용하세요.
tool_call가 실행되기 전에 pi는 이전에 발생한 Agent 이벤트가 AgentSession을 통해 배수를 완료할 때까지 기다립니다. 이는 ctx.sessionManager가 현재 보조 도구 호출 메시지를 통해 최신 상태임을 의미합니다.
기본 병렬 도구 실행 모드에서는 동일한 보조 메시지의 형제 도구 호출이 순차적으로 사전 실행된 다음 동시에 실행됩니다. tool_call는 ctx.sessionManager의 동일한 보조 메시지에서 형제 도구 결과를 볼 수 있다고 보장되지 않습니다.
event.input는 변경 가능합니다. 실행 전에 도구 인수를 패치하려면 이를 변경하세요.
동작 보장:
event.input에 대한 변형은 실제 도구 실행에 영향을 미칩니다.- 나중에
tool_call핸들러는 이전 핸들러가 만든 변형을 봅니다. - 돌연변이 후에는 재검증이 수행되지 않습니다.
{ block: true, reason?: string, terminate?: boolean }을 통한tool_call제어 차단의 반환 값terminate차단된 통화에만 적용됩니다. 배치의 모든 최종 결과가 종료될 때만 에이전트가 일찍 중지됩니다.
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
pi.on("tool_call", async (event, ctx) => {
// event.toolName - "bash", "read", "write", "edit", etc.
// event.toolCallId
// event.input - tool parameters (mutable)
// Built-in tools: no type params needed
if (isToolCallEventType("bash", event)) {
// event.input is { command: string; timeout?: number }
event.input.command = `source ~/.profile\n${event.input.command}`;
if (event.input.command.includes("rm -rf")) {
return { block: true, reason: "Dangerous command", terminate: true };
}
}
if (isToolCallEventType("read", event)) {
// event.input is { path: string; offset?: number; limit?: number }
console.log(`Reading: ${event.input.path}`);
}
});사용자 정의 도구 입력 입력
사용자 정의 도구는 입력 유형을 내보내야 합니다.
// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;명시적인 유형 매개변수와 함께 isToolCallEventType를 사용하세요.
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";
pi.on("tool_call", (event) => {
if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
event.input.action; // typed
}
});도구_결과
도구 실행이 완료된 후 tool_execution_end 및 최종 도구 결과 메시지 이벤트가 발생하기 전에 실행됩니다. 결과를 수정할 수 있습니다.
병렬 도구 모드에서는 tool_result 및 tool_execution_end가 도구 완료 순서로 인터리브될 수 있지만 최종 toolResult 메시지 이벤트는 나중에 보조 소스 순서로 방출됩니다.
tool_result 미들웨어와 같은 핸들러 체인:
- 핸들러는 확장 로드 순서로 실행됩니다.
- 각 핸들러는 이전 핸들러 변경 후 최신 결과를 확인합니다.
- 핸들러는 부분 패치(
content,details,isError또는usage)를 반환할 수 있습니다. 생략된 필드는 현재 값을 유지합니다.
핸들러 내부의 중첩된 비동기 작업에는 ctx.signal를 사용하세요. 이를 통해 Esc는 모델 호출, fetch() 및 확장 프로그램에서 시작된 기타 중단 인식 작업을 취소할 수 있습니다.
import { isBashToolResult } from "@earendil-works/pi-coding-agent";
pi.on("tool_result", async (event, ctx) => {
// event.toolName, event.toolCallId, event.input
// event.content, event.details, event.isError, event.usage
if (isBashToolResult(event)) {
// event.details is typed as BashToolDetails
}
const response = await fetch("https://example.com/summarize", {
method: "POST",
body: JSON.stringify({ content: event.content }),
signal: ctx.signal,
});
// Modify result:
return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});사용자 배쉬 이벤트
user_bash
사용자가 ! 또는 !! 명령을 실행하면 시작됩니다. 요격할 수 있습니다.
import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";
pi.on("user_bash", (event, ctx) => {
// event.command - the bash command
// event.excludeFromContext - true if !! prefix
// event.cwd - working directory
// Option 1: Provide custom operations (e.g., SSH)
return { operations: remoteBashOps };
// Option 2: Wrap pi's built-in local bash backend
const local = createLocalBashOperations();
return {
operations: {
exec(command, cwd, options) {
return local.exec(`source ~/.profile\n${command}`, cwd, options);
}
}
};
// Option 3: Full replacement - return result directly
return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});입력 이벤트
입력
확장 명령을 확인한 후 스킬 및 템플릿 확장 전에 사용자 입력이 수신되면 실행됩니다. 이벤트는 원시 입력 텍스트를 확인하므로 /skill:foo 및 /template는 아직 확장되지 않습니다.
처리 순서:
- 확장 명령(
/cmd)이 먼저 확인됩니다. 발견되면 핸들러가 실행되고 입력 이벤트를 건너뜁니다. input이벤트 발생 - 가로채기, 변형 또는 처리 가능- 처리하지 않을 경우: 스킬 명령어(
/skill:name)를 스킬 내용으로 확장 - 처리되지 않은 경우: prompt templates(
/template) 템플릿 콘텐츠로 확장 - 에이전트 처리 시작(
before_agent_start등)
pi.on("input", async (event, ctx) => {
// event.text - raw input (before skill/template expansion)
// event.images - attached images, if any
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
// event.streamingBehavior - "steer" | "followUp" | undefined
// undefined when idle, "steer" for mid-stream interrupts,
// "followUp" for messages queued until the agent finishes
// Transform: rewrite input before expansion
if (event.text.startsWith("?quick "))
return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
// Handle: respond without LLM (extension shows its own feedback)
if (event.text === "ping") {
ctx.ui.notify("pong", "info");
return { action: "handled" };
}
// Route by source: skip processing for extension-injected messages
if (event.source === "extension") return { action: "continue" };
// Intercept skill commands before expansion
if (event.text.startsWith("/skill:")) {
// Could transform, block, or let pass through
}
return { action: "continue" }; // Default: pass through to expansion
});결과:
continue- 변경되지 않은 상태로 통과(핸들러가 아무것도 반환하지 않는 경우 기본값)transform- 텍스트/이미지를 수정한 후 계속 확장합니다.handled- 에이전트를 완전히 건너뜁니다(이를 반환하는 첫 번째 핸들러가 승리합니다).
핸들러 전체에서 체인을 변환합니다. streamingBehavior 인식 라우팅은 input-transform.ts 및 input-transform-streaming.ts를 참조하세요.
확장 컨텍스트
모든 핸들러는 ctx: ExtensionContext를 받습니다.
ctx.ui
사용자 상호작용을 위한 UI 메소드. 자세한 내용은 Custom UI를 참조하세요.
ctx.모드
현재 실행 모드: "tui", "rpc", "json" 또는 "print". ctx.mode === "tui"를 사용하면 custom(), 구성 요소 팩토리, 터미널 입력 및 직접 TUI 렌더링과 같은 터미널 전용 기능을 보호할 수 있습니다.
ctx.hasUI
true TUI 및 RPC 모드. false 인쇄 모드(-p) 및 JSON 모드. TUI 및 TUI 모두에서 작동하는 대화 방법(select, confirm, input, editor)과 실행 후 잊어버리는 방법(notify, setStatus, setWidget, setTitle, setEditorText)을 보호하려면 이 기능을 사용하세요. RPC 모드. RPC 모드에서 일부 TUI 관련 메서드는 작동하지 않거나 기본값을 반환합니다(rpc.md 참조).
ctx.cwd
현재 작업 디렉토리.
프로젝트-로컬 구성 경로를 구성할 때 하드코딩 .pi 대신 CONFIG_DIR_NAME를 사용하세요. 브랜드가 변경된 배포판은 다른 구성 디렉터리 이름을 사용할 수 있습니다.
import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
// ...
});
}ctx.isProjectTrusted()
현재 세션 컨텍스트에 대해 프로젝트-로컬 신뢰가 활성화되어 있는지 여부를 반환합니다. 여기에는 글로벌 신뢰 저장소에 저장된 결정뿐만 아니라 임시 신뢰 결정 및 CLI 신뢰 재정의도 포함됩니다.
신뢰할 수 있는 프로젝트에 대해서만 적용되어야 하는 프로젝트-로컬 확장 구성을 읽기 전에 이를 사용하세요.
ctx.sessionManager
세션 상태에 대한 읽기 전용 액세스입니다. 전체 SessionManager API 및 항목 유형은 Session Format를 참조하세요.
tool_call의 경우 이 상태는 핸들러가 실행되기 전에 현재 보조 메시지를 통해 동기화됩니다. 병렬 도구 실행 모드에서는 여전히 동일한 보조 메시지의 형제 도구 결과가 포함된다는 보장이 없습니다.
ctx.sessionManager.getEntries() // All entries
ctx.sessionManager.getBranch() // Current branch
ctx.sessionManager.buildContextEntries() // Active branch entries with compaction applied
ctx.sessionManager.getLeafId() // Current leaf entry IDctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels
모델, 공급자 및 해결된 인증에 대한 액세스입니다. ctx.modelRegistry.getProvider(id)는 효과적인 pi-ai 공급자를 반환하는 반면, getProviderAuth(id)는 로드된 모델을 요구하지 않고 현재 API key, 헤더, 기본 URL 및 공급자 범위 환경을 해결합니다. ctx.model는 능동적 모델이고, ctx.thinkingLevel는 현재 효과적인 사고 수준입니다.
ctx.scopedModels는 현재 세션으로 범위가 지정된 모델의 읽기 전용 목록입니다. /scoped-models 명령이 표시하는 것과 동일한 세트입니다. 세션 시작 시 --models CLI 플래그 및 enabledModels 설정(provider/modelId 또는 베어 modelId의 미니매치가 있는 사용 가능한 카탈로그와 일치)에서 해결됩니다. 범위 지정이 구성되지 않은 경우 비어 있습니다. 즉, 사용 가능한 모든 모델을 사용할 수 있음을 의미합니다. 각 항목은 { model, thinkingLevel? }이며, 여기서 thinkingLevel는 패턴이 고정된 경우에만 설정됩니다(예: anthropic/*:high). ctx.modelRegistry.getAvailable()를 통해 전체 카탈로그를 열거하는 대신 내장 모델을 미러링하는 모델 선택기를 채우는 데 사용하세요.
ctx.신호
현재 에이전트 중단 신호 또는 에이전트 차례가 활성화되지 않은 경우 undefined입니다.
확장 핸들러에 의해 시작된 중단 인식 중첩 작업에 이를 사용하십시오. 예를 들면 다음과 같습니다.
fetch(..., { signal: ctx.signal })signal을 허용하는 모델 호출AbortSignal을 허용하는 파일 또는 프로세스 도우미
ctx.signal는 일반적으로 tool_call, tool_result, message_update 및 turn_end와 같은 활성 턴 이벤트 중에 정의됩니다.
세션 이벤트, 확장 명령, pi가 유휴 상태인 동안 실행되는 바로 가기와 같은 유휴 또는 비순차 컨텍스트에서는 일반적으로 undefined입니다.
pi.on("tool_result", async (event, ctx) => {
const response = await fetch("https://example.com/api", {
method: "POST",
body: JSON.stringify(event),
signal: ctx.signal,
});
const data = await response.json();
return { details: data };
});ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()
제어 흐름 도우미. ctx.isIdle()는 Pi이 에이전트 실행, 자동 재시도, 자동 압축 재시도 또는 대기 중인 연속을 처리하는 동안 false입니다.
ctx.shutdown()
pi의 정상적인 종료를 요청합니다.
- 대화형 모드: 에이전트가 유휴 상태가 될 때까지 연기됩니다(대기 중인 조정 및 후속 메시지를 모두 처리한 후).
- RPC 모드: 다음 유휴 상태까지 연기됩니다(현재 명령 응답 완료 후, 다음 명령을 기다리는 경우).
- 인쇄 모드: 작동하지 않습니다. 모든 프롬프트가 처리되면 프로세스가 자동으로 종료됩니다.
종료하기 전에 모든 확장 프로그램에 session_shutdown 이벤트를 발생시킵니다. 모든 컨텍스트(이벤트 처리기, 도구, 명령, 바로 가기)에서 사용할 수 있습니다.
pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});ctx.getContextUsage()
활성 모델의 현재 컨텍스트 사용량을 반환합니다. 가능한 경우 마지막 어시스턴트 사용량을 사용한 다음 후행 메시지에 대한 토큰을 추정합니다.
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}ctx.compact()
완료를 기다리지 않고 압축을 트리거합니다. 후속 작업에는 onComplete 및 onError를 사용하세요.
ctx.compact({
customInstructions: "Focus on recent changes",
onComplete: (result) => {
ctx.ui.notify("Compaction completed", "info");
},
onError: (error) => {
ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
},
});ctx.getSystemPrompt()
Pi의 현재 시스템 프롬프트 문자열을 반환합니다.
before_agent_start동안 이는 현재 턴에 대해 지금까지 적용된 체인 시스템 프롬프트 변경 사항을 반영합니다.- 이후
context메시지 변형은 포함되지 않습니다. before_provider_request페이로드 재작성은 포함되지 않습니다.- 나중에 로드된 확장 프로그램이 사용자 확장 프로그램 이후에 실행되는 경우 최종적으로 전송되는 내용이 여전히 변경될 수 있습니다.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});ExtensionCommandContext
명령 핸들러는 세션 제어 방법으로 ExtensionContext를 확장하는 ExtensionCommandContext를 수신합니다. 이벤트 핸들러에서 호출하면 교착 상태가 발생할 수 있으므로 명령에서만 사용할 수 있습니다.
ctx.getSystemPromptOptions()
Pi가 현재 시스템 프롬프트를 구축하는 데 사용하는 기본 입력을 반환합니다.
const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];이는 before_agent_start event.systemPromptOptions와 모양 및 변경 가능성이 동일합니다: 사용자 정의 프롬프트, 활성 도구, 도구 조각, 프롬프트 지침, 추가된 시스템 프롬프트 텍스트, cwd, 로드된 context files 및 로드된 기술. 전체 컨텍스트 파일 콘텐츠가 포함될 수 있으므로 민감한 확장 로컬 데이터로 취급하고 명령 목록, 로그 또는 자동 완성 메타데이터를 통해 노출되지 않도록 하세요.
이는 현재 기본 프롬프트 입력을 보고합니다. 턴별 before_agent_start 연결된 시스템 프롬프트 변경, 이후 context 이벤트 메시지 변형 또는 before_provider_request 페이로드 재작성은 포함되지 않습니다.
ctx.waitForIdle()
자동 재시도, 자동 압축 재시도 및 대기 중인 연속 작업을 포함하여 에이전트가 완전히 해결될 때까지 기다립니다.
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.newSession(옵션?)
새 세션을 만듭니다.
const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";
const result = await ctx.newSession({
parentSession,
setup: async (sm) => {
sm.appendMessage({
role: "user",
content: [{ type: "text", text: "Context from previous session..." }],
timestamp: Date.now(),
});
},
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
await ctx.sendUserMessage(kickoff);
},
});
if (result.cancelled) {
// An extension cancelled the new session
}옵션:
parentSession: 새 세션 헤더에 기록할 상위 세션 파일setup:withSession가 실행되기 전에 새 세션의SessionManager를 변경합니다.withSession: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전pi/ 명령ctx을 사용하지 마세요. Session replacement lifecycle and footguns를 참조하세요.
ctx.fork(entryId, 옵션?)
특정 항목에서 분기하여 새 세션 파일을 만듭니다.
const result = await ctx.fork("entry-id-123", {
withSession: async (ctx) => {
// Use only the replacement-session ctx here.
ctx.ui.notify("Now in the forked session", "info");
},
});
if (result.cancelled) {
// An extension cancelled the fork
}
const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
// An extension cancelled the clone
}옵션:
position:"before"(기본값) 선택한 사용자 메시지 앞에서 포크하여 해당 메시지를 편집기에 복원합니다.position:"at"편집기 텍스트를 복원하지 않고 선택한 항목을 통해 활성 경로를 복제합니다.withSession: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전pi/ 명령ctx을 사용하지 마세요. Session replacement lifecycle and footguns를 참조하세요.
ctx.navigateTree(targetId, 옵션?)
session tree에서 다른 지점으로 이동합니다.
const result = await ctx.navigateTree("entry-id-456", {
summarize: true,
customInstructions: "Focus on error handling changes",
replaceInstructions: false, // true = replace default prompt entirely
label: "review-checkpoint",
});옵션:
summarize: 버려진 브랜치의 요약을 생성할지 여부customInstructions: 요약기에 대한 사용자 정의 지침replaceInstructions: true인 경우customInstructions는 추가되는 대신 기본 프롬프트를 대체합니다.label: 분기 요약 항목(또는 요약하지 않는 경우 대상 항목)에 첨부할 라벨
ctx.switchSession(세션 경로, 옵션?)
다른 세션 파일로 전환합니다.
const result = await ctx.switchSession("/path/to/session.jsonl", {
withSession: async (ctx) => {
await ctx.sendUserMessage("Resume work in the replacement session");
},
});
if (result.cancelled) {
// An extension cancelled the switch via session_before_switch
}옵션:
withSession: 새로운 교체 세션 컨텍스트에 대해 전환 후 작업을 실행합니다. 캡처된 이전pi/ 명령ctx을 사용하지 마세요. Session replacement lifecycle and footguns를 참조하세요.
사용 가능한 세션을 검색하려면 정적 SessionManager.list() 또는 SessionManager.listAll() 메소드를 사용하세요.
import { SessionManager } from "@earendil-works/pi-coding-agent";
pi.registerCommand("switch", {
description: "Switch to another session",
handler: async (args, ctx) => {
const sessions = await SessionManager.list(ctx.cwd);
if (sessions.length === 0) return;
const choice = await ctx.ui.select(
"Pick session:",
sessions.map(s => s.file),
);
if (choice) {
await ctx.switchSession(choice, {
withSession: async (ctx) => {
ctx.ui.notify("Switched session", "info");
},
});
}
},
});세션 교체 수명주기 및 풋건
withSession는 대체 세션에 바인딩된 비동기 sendMessage() 및 sendUserMessage() 도우미로 ExtensionCommandContext를 확장하는 새로운 ReplacedSessionContext를 받습니다.
라이프사이클 및 풋건:
withSession는 이전 세션이 실행되고(session_shutdown), 이전 런타임이 해제되고, 대체 세션이 리바운드되고, 새 확장 인스턴스가 이미session_start를 수신한 후에만 실행됩니다.- 콜백은 새 확장 인스턴스 내부가 아닌 원래 클로저에서 계속 실행됩니다. 이는
withSession가 시작되기 전에 이전 확장 프로그램 인스턴스가 이미 종료 정리를 실행했을 수 있음을 의미합니다. - 캡처된 이전
pi/ 이전 명령ctx세션 바인딩 개체는 교체 후 오래되었으며 사용하면 오류가 발생합니다. 세션 바인딩 작업의 경우withSession에 전달된ctx만 사용하세요. - 이전에 추출한 원시 개체는 여전히 귀하의 책임입니다. 예를 들어 교체하기 전에
const sm = ctx.sessionManager를 캡처하면sm는 여전히 이전SessionManager개체입니다. 교체 후 재사용하지 마십시오. withSession의 코드는session_shutdown핸들러에 의해 무효화된 상태가 이미 사라진 것으로 가정해야 합니다. 문자열, ID, 직렬화된 구성 등 종료 후에도 깨끗하게 유지되는 일반 데이터만 캡처합니다.
안전한 패턴:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});안전하지 않은 패턴:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const oldSessionManager = ctx.sessionManager;
await ctx.newSession({
withSession: async (_ctx) => {
// stale old objects: do not do this
oldSessionManager.getSessionFile();
pi.sendUserMessage("wrong");
},
});
},
});ctx.reload()
/reload와 동일한 다시 로드 흐름을 실행합니다.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});중요한 동작:
await ctx.reload()는 현재 확장 런타임에 대해session_shutdown을 내보냅니다.- 그런 다음 리소스를 다시 로드하고
reason: "reload"로session_start을 내보내고 이유"reload"로resources_discover를 내보냅니다. - 현재 실행 중인 명령 처리기는 여전히 이전 호출 프레임에서 계속됩니다.
await ctx.reload()이후의 코드는 다시 로드 전 버전에서 계속 실행됩니다.await ctx.reload()이후의 코드는 이전 메모리 내 확장 상태가 여전히 유효하다고 가정하면 안 됩니다.- 핸들러가 반환된 후 향후 명령/이벤트/도구 호출에서는 새 확장 버전을 사용합니다.
예측 가능한 동작을 위해 다시 로드를 해당 핸들러의 터미널로 처리하십시오(await ctx.reload(); return;).
도구는 ExtensionContext로 실행되므로 ctx.reload()를 직접 호출할 수 없습니다. 명령을 다시 로드 진입점으로 사용한 다음 해당 명령을 후속 사용자 메시지로 대기열에 추가하는 도구를 노출합니다.
LLM이 다시 로드를 트리거하기 위해 호출할 수 있는 예제 도구:
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});
pi.registerTool({
name: "reload_runtime",
label: "Reload Runtime",
description: "Reload extensions, skills, prompts, themes, and context files",
parameters: Type.Object({}),
async execute() {
pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
return {
content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
};
},
});
}확장API 방법
pi.on(이벤트, 핸들러)
이벤트를 구독하세요. 이벤트 유형 및 반환 값은 Events를 참조하세요.
pi.registerTool(정의)
LLM에서 호출할 수 있는 사용자 정의 도구를 등록합니다. 자세한 내용은 Custom Tools를 참조하세요.
pi.registerTool() 확장 로드 중과 시작 후에 모두 작동합니다. session_start, 명령 핸들러 또는 기타 이벤트 핸들러 내에서 호출할 수 있습니다. 새로운 도구는 동일한 세션에서 즉시 새로 고쳐지므로 pi.getAllTools()에 표시되고 /reload 없이 LLM에서 호출할 수 있습니다.
런타임에 도구(동적으로 추가된 도구 포함)를 활성화하거나 비활성화하려면 pi.setActiveTools()를 사용하세요.
promptSnippet를 사용하여 Available tools의 한 줄 항목에 사용자 정의 도구를 선택하고, promptGuidelines를 사용하여 도구가 활성화되었을 때 기본 Guidelines 섹션에 도구별 글머리 기호를 추가합니다.
중요: promptGuidelines 글머리 기호는 도구 이름 접두사 없이 Guidelines 섹션에 추가됩니다. 각 지침은 참조하는 도구의 이름을 지정해야 합니다. LLM은 "이"가 어떤 도구를 의미하는지 알 수 없기 때문에 "...일 때 이 도구를 사용하십시오."를 피하십시오. 대신 "...일 때 my_tool 사용"이라고 작성하세요.
전체 예를 보려면 dynamic-tools.ts를 참조하세요.
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does",
promptSnippet: "Summarize or transform text according to action",
promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
// Optional compatibility shim. Runs before schema validation.
// Return the current schema shape, for example to fold legacy fields
// into the modern parameter object.
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Stream progress
onUpdate?.({ content: [{ type: "text", text: "Working..." }] });
return {
content: [{ type: "text", text: "Done" }],
details: { result: "..." },
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});pi.sendMessage(메시지, 옵션?)
세션에 사용자 정의 메시지를 삽입합니다. 사용자 정의 메시지는 LLM 컨텍스트에 참여합니다. LLM으로 보내면 안 되는 내구성 있는 TUI 전용 콘텐츠의 경우 pi.appendEntry()를 pi.registerEntryRenderer()와 함께 사용하세요.
pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});옵션:
deliverAs- 전달 모드:"steer"(기본값) - 스트리밍하는 동안 메시지를 대기열에 넣습니다. 현재 보조 차례가 도구 호출 실행을 마친 후 다음 LLM 호출 전에 전달됩니다."followUp"- 에이전트가 완료될 때까지 기다립니다. 상담원이 더 이상 도구 호출을 하지 않는 경우에만 전달됩니다."nextTurn"- 다음 사용자 프롬프트를 위해 대기 중입니다. 아무것도 방해하거나 트리거하지 않습니다.
triggerTurn: true- 에이전트가 유휴 상태인 경우 즉시 LLM 응답을 트리거합니다."steer"및"followUp"모드에만 적용됩니다("nextTurn"의 경우 무시됨).
pi.sendUserMessage(콘텐츠, 옵션?)
사용자 메시지를 에이전트에 보냅니다. 맞춤 메시지를 보내는 sendMessage()와 달리, 사용자가 입력한 것처럼 실제 사용자에게 메시지를 보냅니다. 항상 회전을 유발합니다.
// Simple text message
pi.sendUserMessage("What is 2+2?");
// With content array (text + images)
pi.sendUserMessage([
{ type: "text", text: "Describe this image:" },
{ type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);
// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });옵션:
deliverAs- 에이전트가 스트리밍 중일 때 필수:"steer"- 현재 보조 턴이 도구 호출 실행을 마친 후 전달을 위해 메시지를 대기열에 넣습니다."followUp"- 상담원이 모든 도구를 완료할 때까지 기다립니다.
스트리밍하지 않을 때는 메시지가 즉시 전송되고 새로운 차례가 시작됩니다. deliverAs 없이 스트리밍하면 오류가 발생합니다.
전체 예를 보려면 send-user-message.ts를 참조하세요.
pi.appendEntry(customType, 데이터?)
확장 데이터를 유지합니다. 사용자 정의 항목은 LLM 컨텍스트에 참여하지 않습니다. 대화형 모드에서는 pi.registerEntryRenderer()와 함께 사용하면 채팅 내용 내에서 렌더링할 수도 있습니다.
pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });
// Restore on reload
pi.on("session_start", async (_event, ctx) => {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "custom" && entry.customType === "my-state") {
// Reconstruct from entry.data
}
}
});pi.setSessionName(이름)
세션 표시 이름을 설정합니다(첫 번째 메시지 대신 세션 선택기에 표시됨).
pi.setSessionName("Refactor auth module");pi.getSessionName()
설정된 경우 현재 세션 이름을 가져옵니다.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, 라벨)
항목의 레이블을 설정하거나 지웁니다. 라벨은 북마크 및 탐색을 위한 사용자 정의 마커입니다(/tree 선택기에 표시됨).
// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");
// Clear a label
pi.setLabel(entryId, undefined);
// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);레이블은 세션에서 유지되며 다시 시작해도 유지됩니다. 대화 트리에서 중요한 지점(전환, 체크포인트)을 표시하는 데 사용합니다.
pi.registerCommand(이름, 옵션)
명령어를 등록합니다.
여러 확장이 동일한 명령 이름을 등록하는 경우 pi는 이를 모두 유지하고 로드 순서에 따라 숫자 호출 접미사를 할당합니다(예: /review:1 및 /review:2).
pi.registerCommand("stats", {
description: "Show session statistics",
handler: async (args, ctx) => {
const count = ctx.sessionManager.getEntries().length;
ctx.ui.notify(`${count} entries`, "info");
}
});선택 사항: /command...에 대한 인수 자동 완성을 추가합니다.
import type { AutocompleteItem } from "@earendil-works/pi-tui";
pi.registerCommand("deploy", {
description: "Deploy to an environment",
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
const envs = ["dev", "staging", "prod"];
const items = envs.map((e) => ({ value: e, label: e }));
const filtered = items.filter((i) => i.value.startsWith(prefix));
return filtered.length > 0 ? filtered : null;
},
handler: async (args, ctx) => {
ctx.ui.notify(`Deploying: ${args}`, "info");
},
});pi.getCommands()
현재 세션에서 prompt를 통해 호출 가능한 slash commands을 가져옵니다. 확장 명령어, prompt templates, 스킬 명령어가 포함됩니다.
목록은 RPC get_commands 순서와 일치합니다. 확장 기능이 먼저, 템플릿이 먼저, 기술이 그 다음입니다.
const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");각 항목의 모양은 다음과 같습니다.
{
name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
description?: string;
source: "extension" | "prompt" | "skill";
sourceInfo: {
path: string;
source: string;
scope: "user" | "project" | "temporary";
origin: "package" | "top-level";
baseDir?: string;
};
}sourceInfo를 정식 출처 필드로 사용하세요. 명령 이름이나 임시 경로 구문 분석에서 소유권을 추론하지 마십시오.
내장된 대화형 명령(예: /model 및 /settings)은 여기에 포함되지 않습니다. 대화형으로만 처리됩니다.
모드이며 prompt를 통해 전송되면 실행되지 않습니다.
pi.registerMessageRenderer(customType, 렌더러)
customType에 사용자 정의 메시지를 위한 사용자 정의 TUI 렌더러를 등록하세요. 맞춤 메시지는 pi.sendMessage()를 사용하여 생성되고 LLM 컨텍스트에 참여합니다. Custom UI를 참조하세요.
pi.registerMarkdownTransformer(변압기)
일반 사용자 텍스트, 보조 텍스트 및 사고 블록에 Markdown에 대한 변환기를 등록합니다. Transformer는 확장 로드 순서로 실행되며 각 Transformer는 이전 Transformer에서 반환된 Markdown를 받습니다. 체인이 완료된 후 Pi는 내장 렌더러를 사용하여 변환된 콘텐츠를 렌더링합니다.
변환기는 Markdown 문자열과 다음과 같은 컨텍스트를 수신합니다.
messageType—"user","assistant"또는"assistant-thinking"isStreaming—true부분 어시스턴트 업데이트의 경우;false사용자, 최종 어시스턴트, 복원된 메시지용availableWidth— 변환된 Markdown 콘텐츠에 사용할 수 있는 정확한 터미널 열
변환된 Markdown를 반환합니다.
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});변환기가 던지면 Pi는 지금까지 생성된 Markdown을 유지하고 다음 변환기를 계속합니다. 후크는 표시 전용입니다. 원래 메시지는 세션 및 모델 컨텍스트에서 변경되지 않은 상태로 유지됩니다. 새로운 사용자 메시지, 보조 스트리밍 업데이트, 복원된 세션 메시지 및 터미널 너비 변경에 대해 실행되므로 변환기는 동기식을 유지하고 저렴해야 합니다.
pi.registerEntryRenderer(customType, 렌더러)
customType에 사용자 정의 항목을 위한 사용자 정의 TUI 렌더러를 등록하세요. 사용자 정의 항목은 pi.appendEntry()로 생성되며 LLM 컨텍스트에 참여하지 않습니다.
import { Box, Text } from "@earendil-works/pi-tui";
pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
const data = entry.data as { title: string; count: number };
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
if (expanded) {
box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
}
return box;
});
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });pi.registerShortcut(단축키, 옵션)
키보드 단축키를 등록하세요. 단축키 형식과 내장된 키 바인딩은 keybindings.md를 참조하세요.
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});pi.registerFlag(이름, 옵션)
CLI 플래그를 등록합니다.
pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}pi.exec(명령어, 인수, 옵션?)
쉘 명령을 실행합니다.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(이름)
활성 도구를 관리합니다. 이는 내장 도구와 동적으로 등록된 도구 모두에 적용됩니다. pi.getActiveTools()는 활성 도구 이름을 string[]로 반환합니다. pi.getAllTools()는 구성된 모든 도구에 대한 메타데이터를 반환합니다.
const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
// name: "read",
// description: "Read file contents...",
// parameters: ...,
// promptGuidelines: ["Use read to examine files instead of cat or sed."],
// sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-onlypi.getAllTools()는 name, description, parameters, promptGuidelines 및 sourceInfo를 반환합니다.
일반적인 sourceInfo.source 값:
builtin내장 도구sdkcreateAgentSession({ customTools })를 통해 전달된 도구의 경우- 확장으로 등록된 도구에 대한 확장 소스 메타데이터
pi.setModel(모델)
현재 모델을 설정합니다. 모델에 API key를 사용할 수 없는 경우 false를 반환합니다. 사용자 정의 모델을 구성하려면 models.md를 참조하세요.
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}pi.getThinkingLevel() / pi.setThinkingLevel(레벨)
사고 수준을 얻거나 설정하십시오. 수준은 모델 기능에 따라 고정됩니다(비추론 모델은 항상 "off"를 사용함). 변경 사항은 thinking_level_select를 내보냅니다.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");파이.이벤트
확장 간 통신을 위한 공유 이벤트 버스:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(이름, 구성)
모델 공급자를 동적으로 등록하거나 재정의합니다. 프록시, 사용자 정의 엔드포인트 또는 팀 전체 모델 구성에 유용합니다.
확장 팩토리 기능 중에 이루어진 호출은 대기열에 추가되고 실행기가 초기화되면 적용됩니다. 그 이후에 이루어진 호출(예: 사용자 설정 흐름에 따른 명령 처리기)은 /reload 없이도 즉시 적용됩니다.
동적 공급자는 refreshModels을 구현할 수 있습니다. Pi는 모델 새로 고침 중에 이를 호출하고, 반환된 목록을 공급자를 통해 동기적으로 게시하고, 정식 자격 증명/저장된 카탈로그/네트워크/신호 컨텍스트를 전달합니다. 확장은 생성 확인 context.publish({ persist: entry })을 통해 카탈로그 메타데이터를 유지할지 여부를 결정합니다. llama.cpp와 같은 라이브 서버는 모델을 유지하지 않고 모델을 반환할 수 있습니다.
context.signal는 항상 구체적인 신호이며 공급자 콜백은 이를 차단 I/O에 전달해야 합니다. 공개 ModelRuntime.refresh() 및 ModelRegistry.refresh() 호출은 선택적 신호를 허용하며 생략되면 제한이 없습니다. 확장 프로그램과 응용 프로그램은 자체 마감일을 선택합니다. 취소하면 공급자가 신호를 무시하더라도 발신자가 기다리는 것을 중지하지만 기본 작업을 중지하려면 여전히 협력이 필요합니다.
Extensions 기본 공급자 인증, 필터링, 새로 고침 또는 스트림 동작이 필요한 경우 @earendil-works/pi-ai에서 전체 Provider를 등록할 수 있습니다. 공급자는 구성 기반이 되며 그 위에 models.json 재정의가 계속 적용됩니다.
import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";
const provider = createProvider({
id: "local-server",
name: "Local Server",
baseUrl: "http://localhost:8080/v1",
auth: {
apiKey: {
name: "Local server setup",
async login(interaction) {
return {
type: "api_key",
key: await interaction.prompt({ type: "secret", message: "API key" }),
};
},
async resolve({ credential }) {
return credential?.key
? { auth: { apiKey: credential.key }, source: "stored API key" }
: undefined;
},
},
},
models: [],
api: openAICompletionsApi(),
});
pi.registerProvider(provider);
// Register a new provider with custom models
pi.registerProvider("my-proxy", {
name: "My Proxy",
baseUrl: "https://proxy.example.com",
apiKey: "$PROXY_API_KEY", // env var reference
api: "anthropic-messages",
models: [
{
id: "claude-sonnet-4-20250514",
name: "Claude 4 Sonnet (proxy)",
reasoning: false,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 16384
}
]
});
// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
baseUrl: "http://localhost:8080/v1",
apiKey: "local",
api: "openai-completions",
async refreshModels({ signal }) {
const response = await fetch("http://localhost:8080/v1/models", { signal });
const { data } = await response.json();
return data.map(({ id }) => ({
id,
name: id,
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384
}));
}
});
// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
baseUrl: "https://proxy.example.com"
});
// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
baseUrl: "https://ai.corp.com",
api: "openai-responses",
models: [...],
oauth: {
name: "Corporate AI (SSO)",
async login(callbacks) {
// Custom OAuth flow
callbacks.onAuth({ url: "https://sso.corp.com/..." });
const code = await callbacks.onPrompt({ message: "Enter code:" });
return { refresh: code, access: code, expires: Date.now() + 3600000 };
},
async refreshToken(credentials, signal) {
signal.throwIfAborted();
// Refresh logic
return credentials;
},
getApiKey(credentials) {
return credentials.access;
}
}
});개체 양식은 기본 auth, getModels, refreshModels, filterModels, stream 및 streamSimple 동작을 포함하여 완전한 pi-ai Provider를 허용합니다.
기존 구성 옵션:
name-/login와 같은 UI의 공급자 표시 이름입니다.baseUrl- API 엔드포인트 URL. 모델을 정의할 때 필요합니다.apiKey- API key 리터럴, 환경 보간($ENV_VAR또는${ENV_VAR}) 또는 선행!command. 모델을 정의할 때 필요합니다(oauth가 제공되지 않은 경우).$는 ``apiKey- API key 리터럴, 환경 보간($ENV_VAR또는${ENV_VAR}) 또는 선행!command. 모델을 정의할 때 필요합니다(oauth가 제공되지 않은 경우).$는 를 이스케이프하고,$!는 명령 실행을 트리거하지 않고 리터럴!`을 이스케이프합니다.api- API 유형:"anthropic-messages","openai-completions","openai-responses"등headers- 요청에 포함할 맞춤 헤더입니다.authHeader- true인 경우Authorization: Bearer헤더를 자동으로 추가합니다.models- 모델 정의 배열. 제공된 경우 이 공급자의 기존 모델을 모두 바꿉니다. 모델 정의는baseUrl를 설정하여 해당 모델의 공급자 엔드포인트를 재정의할 수 있습니다.refreshModels- 비동기 동적 검색 콜백. 반환된 모델은 확장 제공 모델을 대체합니다.context.stored에는 지속된 공급자 스냅샷이 포함되어 있습니다. 업데이트된 카탈로그 데이터가 지속되어야 하는 경우에만 생성 확인context.publish({ persist: entry })을 사용하세요. 해당 스냅샷을 삭제하려면persist: null를 사용하세요.oauth-/login지원을 위한 OAuth 공급자 구성입니다. 제공되면 공급자가 로그인 메뉴에 나타납니다.streamSimple- 비표준 API에 대한 맞춤 스트리밍 구현입니다.
고급 주제는 custom-provider.md를 참조하세요: 사용자 정의 스트리밍 APIs, OAuth 세부 정보, 모델 정의 참조.
pi.unregisterProvider(이름)
이전에 등록된 공급자와 해당 모델을 제거합니다. 공급자가 재정의한 기본 제공 모델이 복원됩니다. 공급자가 등록되지 않은 경우 아무런 효과가 없습니다.
registerProvider와 마찬가지로 초기 로드 단계 이후에 호출되면 즉시 적용되므로 /reload가 필요하지 않습니다.
pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});상태 관리
Extensions 상태는 적절한 분기 지원을 위해 도구 결과 details에 저장해야 합니다.
export default function (pi: ExtensionAPI) {
let items: string[] = [];
// Reconstruct state from session
pi.on("session_start", async (_event, ctx) => {
items = [];
for (const entry of ctx.sessionManager.getBranch()) {
if (entry.type === "message" && entry.message.role === "toolResult") {
if (entry.message.toolName === "my_tool") {
items = entry.message.details?.items ?? [];
}
}
}
});
pi.registerTool({
name: "my_tool",
// ...
async execute(toolCallId, params, signal, onUpdate, ctx) {
items.push("new item");
return {
content: [{ type: "text", text: "Added" }],
details: { items: [...items] }, // Store for reconstruction
};
},
});
}맞춤형 도구
pi.registerTool()를 통해 LLM이 호출할 수 있는 도구를 등록하세요. 도구는 시스템 프롬프트에 표시되며 사용자 정의 렌더링을 가질 수 있습니다.
기본 시스템 프롬프트의 Available tools 섹션에 짧은 한 줄 항목을 입력하려면 promptSnippet를 사용하세요. 생략하면 사용자 정의 도구가 해당 섹션에서 제외됩니다.
기본 시스템 프롬프트 Guidelines 섹션에 도구별 글머리 기호를 추가하려면 promptGuidelines를 사용하세요. 이러한 글머리 기호는 도구가 활성화된 동안에만 포함됩니다(예: pi.setActiveTools([...]) 이후).
중요: promptGuidelines 글머리 기호는 도구 이름 접두사 또는 그룹화 없이 Guidelines 섹션에 단순하게 추가됩니다. 각 지침은 참조하는 도구의 이름을 지정해야 합니다. LLM은 "이"가 어떤 도구를 의미하는지 알 수 없기 때문에 "...일 때 이 도구를 사용하십시오."를 피하십시오. 대신 "...일 때 my_tool 사용"이라고 작성하세요.
참고: 일부 모델은 바보이며 도구 경로 인수에 @ 접두사를 포함합니다. 내장 도구는 경로를 확인하기 전에 선행 @를 제거합니다. 사용자 정의 도구가 경로를 허용하는 경우 선행 @도 정규화하세요.
사용자 정의 도구가 파일을 변경하는 경우 withFileMutationQueue()를 사용하여 기본 제공 edit 및 write와 동일한 파일별 대기열에 참여하도록 합니다. 도구 호출은 기본적으로 병렬로 실행되기 때문에 이는 중요합니다. 대기열이 없으면 두 도구가 동일한 이전 파일 내용을 읽고, 서로 다른 업데이트를 계산한 다음, 마지막 쓰기 토지가 다른 도구를 덮어쓸 수 있습니다.
실패 사례 예: 사용자 정의 도구가 내장된 동안 foo.ts을 편집하고 edit도 동일한 보조 회전에서 foo.ts를 변경합니다. 도구가 대기열에 참여하지 않으면 둘 다 원본 foo.ts을 읽고 별도의 변경 사항을 적용할 수 있으며 해당 변경 사항 중 하나가 손실됩니다.
원시 사용자 인수가 아닌 실제 대상 파일 경로를 withFileMutationQueue()에 전달합니다. 먼저 ctx.cwd 또는 도구의 작업 디렉터리를 기준으로 절대 경로로 해결하세요. 기존 파일의 경우 도우미는 realpath()를 통해 정규화하므로 동일한 파일에 대한 심볼릭 링크 별칭은 하나의 대기열을 공유합니다. 새 파일의 경우 아직 realpath()에 대한 내용이 없기 때문에 확인된 절대 경로로 대체됩니다.
해당 대상 경로에 전체 돌연변이 창을 대기열에 추가합니다. 여기에는 최종 쓰기뿐만 아니라 읽기-수정-쓰기 논리도 포함됩니다.
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const absolutePath = resolve(ctx.cwd, params.path);
return withFileMutationQueue(absolutePath, async () => {
await mkdir(dirname(absolutePath), { recursive: true });
const current = await readFile(absolutePath, "utf8");
const next = current.replace(params.oldText, params.newText);
await writeFile(absolutePath, next, "utf8");
return {
content: [{ type: "text", text: `Updated ${params.path}` }],
details: {},
};
});
}도구 정의
import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
promptGuidelines: [
"Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
],
parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as { action?: string; oldAction?: string };
if (typeof input.oldAction === "string" && input.action === undefined) {
return { ...input, action: input.oldAction };
}
return args;
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// Check for cancellation
if (signal?.aborted) {
return { content: [{ type: "text", text: "Cancelled" }] };
}
// Stream progress updates
onUpdate?.({
content: [{ type: "text", text: "Working..." }],
details: { progress: 50 },
});
// Run commands via pi.exec (captured from extension closure)
const result = await pi.exec("some-command", [], { signal });
// Return result
return {
content: [{ type: "text", text: "Done" }], // Sent to LLM
details: { data: result }, // For rendering & state
// usage: nestedModelResponse.usage, // Optional nested LLM usage
// Optional: stop after this tool batch when every finalized tool result
// in the batch also returns terminate: true.
terminate: true,
};
},
// Optional: Custom rendering
renderCall(args, theme, context) { ... },
renderResult(result, options, theme, context) { ... },
});사용 계산: 도구가 중첩된 LLM 호출을 수행하는 경우 결합된 Usage를 usage로 반환합니다. Pi는 도구 결과에 이를 유지하고 바닥글, /session 및 RPC 세션 총계에 포함합니다. tool_result 핸들러는 이 값을 검사하거나 바꿀 수 있습니다.
신호 오류: 도구 실행을 실패로 표시하려면(결과에 isError: true을 설정하고 LLM에 보고) execute에서 오류를 발생시킵니다. 값을 반환하면 반환 개체에 포함된 속성에 관계없이 오류 플래그가 설정되지 않습니다.
조기 종료: execute()에서 terminate: true를 반환하여 현재 도구 배치 후에 자동 후속 LLM 호출을 건너뛰어야 함을 암시합니다. 이는 해당 배치의 모든 최종 도구 결과가 종료될 때만 적용됩니다. 에이전트가 최종 구조화된 출력 도구 호출로 끝나는 최소 예는 examples/extensions/structured-output.ts를 참조하세요.
// Correct: throw to signal an error
async execute(toolCallId, params) {
if (!isValid(params.input)) {
throw new Error(`Invalid input: ${params.input}`);
}
return { content: [{ type: "text", text: "OK" }], details: {} };
}중요: 문자열 열거형에는 @earendil-works/pi-ai에서 StringEnum를 사용하세요. Type.Union/Type.Literal는 Google의 API와 작동하지 않습니다.
인수 준비: prepareArguments(args)는 선택 사항입니다. 정의된 경우 스키마 유효성 검사 이전과 execute() 이전에 실행됩니다. pi가 저장된 도구 호출 인수가 더 이상 현재 스키마와 일치하지 않는 이전 세션을 재개할 때 이를 사용하여 이전에 허용된 입력 형태를 모방합니다. parameters에 대해 유효성을 검사하려는 개체를 반환합니다. 공개 스키마를 엄격하게 유지하세요. 이전에 재개된 세션이 계속 작동하도록 하기 위해 더 이상 사용되지 않는 호환성 필드를 parameters에 추가하지 마세요.
예: 이전 세션에는 최상위 oldText 및 newText가 포함된 edit 도구 호출이 포함될 수 있지만 현재 스키마는 edits: [{ oldText, newText }]만 허용합니다.
pi.registerTool({
name: "edit",
label: "Edit",
description: "Edit a single file using exact text replacement",
parameters: Type.Object({
path: Type.String(),
edits: Type.Array(
Type.Object({
oldText: Type.String(),
newText: Type.String(),
}),
),
}),
prepareArguments(args) {
if (!args || typeof args !== "object") return args;
const input = args as {
path?: string;
edits?: Array<{ oldText: string; newText: string }>;
oldText?: unknown;
newText?: unknown;
};
if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
return args;
}
return {
...input,
edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
};
},
async execute(toolCallId, params, signal, onUpdate, ctx) {
// params now matches the current schema
return {
content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
details: {},
};
},
});내장 도구 재정의
Extensions는 동일한 이름의 도구를 등록하여 내장 도구(read, bash, edit, write, grep, find, ls)를 재정의할 수 있습니다. 이런 일이 발생하면 대화형 모드에서 경고를 표시합니다.
# Extension's read tool replaces built-in read
pi -e ./tool-override.ts또는 --no-builtin-tools를 사용하여 확장 도구를 활성화한 상태에서 기본 제공 도구 없이 시작하세요.
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts로깅 및 액세스 제어로 read를 재정의하는 전체 예는 examples/extensions/tool-override.ts를 참조하세요.
렌더링: 내장 렌더러 상속은 슬롯별로 해결됩니다. 실행 재정의와 렌더링 재정의는 독립적입니다. 재정의에서 renderCall가 생략되면 내장된 renderCall가 사용됩니다. 재정의에서 renderResult를 생략하면 내장된 renderResult가 사용됩니다. 재정의에서 두 가지를 모두 생략하면 내장 렌더러가 자동으로 사용됩니다(구문 강조 표시, diff 등). 이를 통해 UI를 다시 구현하지 않고도 로깅 또는 액세스 제어를 위한 내장 도구를 래핑할 수 있습니다.
프롬프트 메타데이터: promptSnippet 및 promptGuidelines는 내장 도구에서 상속되지 않습니다. 재정의에서 해당 프롬프트 지침을 유지해야 하는 경우 재정의에 명시적으로 정의하세요.
구현은 details 유형을 포함하여 정확한 결과 형태와 일치해야 합니다. UI 및 세션 논리는 렌더링 및 상태 추적을 위해 이러한 모양에 따라 달라집니다.
내장 도구 구현:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
원격 실행
내장된 도구는 원격 시스템(SSH, 컨테이너 등)에 위임하기 위한 플러그형 작업을 지원합니다.
import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";
// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
operations: {
readFile: (path) => sshExec(remote, `cat ${path}`),
access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
}
});
// Register, checking flag at execution time
pi.registerTool({
...remoteRead,
async execute(id, params, signal, onUpdate, _ctx) {
const ssh = getSshConfig();
if (ssh) {
const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
return tool.execute(id, params, signal, onUpdate);
}
return localRead.execute(id, params, signal, onUpdate);
},
});작업 인터페이스: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
user_bash의 경우 확장 프로그램은 로컬 프로세스 생성, 셸 확인 및 프로세스 트리 종료를 다시 구현하는 대신 createLocalBashOperations()를 통해 pi의 로컬 셸 백엔드를 재사용할 수 있습니다.
bash 도구는 실행 전에 명령, cwd 또는 env를 조정하기 위한 생성 후크도 지원합니다.
import { createBashTool } from "@earendil-works/pi-coding-agent";
const bashTool = createBashTool(cwd, {
spawnHook: ({ command, cwd, env }) => ({
command: `source ~/.profile\n${command}`,
cwd: `/mnt/sandbox${cwd}`,
env: { ...env, CI: "1" },
}),
});createBashTool()는 PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL 및 PI_REASONING_LEVEL를 통해 명령에 현재 세션을 노출합니다. 주입은 spawnHook 이전에 발생하므로 후크는 env에서 이러한 값을 수신하고 위와 같이 기존 환경을 확산할 때 이를 보존합니다. 비활성화하려면 exposeSessionEnvironment: false를 설정하세요.
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});변수 의미는 Bash tool session environment를 참조하세요. --ssh 플래그가 포함된 전체 SSH 예시는 examples/extensions/ssh.ts를 참조하세요.
출력 잘림
도구는 LLM 컨텍스트를 압도하지 않도록 출력을 잘라야 합니다. 출력이 크면 다음이 발생할 수 있습니다.
- 컨텍스트 오버플로 오류(프롬프트가 너무 김)
- 압축 실패
- 저하된 모델 성능
기본 제공 제한은 50KB(~10,000개 토큰) 및 2000줄 중 먼저 도달하는 것입니다. 내보낸 자르기 유틸리티를 사용합니다.
import {
truncateHead, // Keep first N lines/bytes (good for file reads, search results)
truncateTail, // Keep last N lines/bytes (good for logs, command output)
truncateLine, // Truncate a single line to maxBytes with ellipsis
formatSize, // Human-readable size (e.g., "50KB", "1.5MB")
DEFAULT_MAX_BYTES, // 50KB
DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";
async execute(toolCallId, params, signal, onUpdate, ctx) {
const output = await runCommand();
// Apply truncation
const truncation = truncateHead(output, {
maxLines: DEFAULT_MAX_LINES,
maxBytes: DEFAULT_MAX_BYTES,
});
let result = truncation.content;
if (truncation.truncated) {
// Write full output to temp file
const tempFile = writeTempFile(output);
// Inform the LLM where to find complete output
result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
result += ` Full output saved to: ${tempFile}]`;
}
return { content: [{ type: "text", text: result }] };
}핵심 사항:
- 시작이 중요한 콘텐츠(검색 결과, 파일 읽기)에는
truncateHead를 사용하세요. - 끝이 중요한 콘텐츠(로그, 명령 출력)에는
truncateTail를 사용하세요. - 출력이 잘린 경우 항상 LLM에 알리고 전체 버전을 찾을 수 있는 위치를 알려주세요.
- 도구 설명에 잘림 제한을 문서화하세요.
적절한 잘림으로 rg(ripgrep)을 래핑하는 전체 예제는 examples/extensions/truncated-tool.ts를 참조하세요.
여러 도구
하나의 확장은 공유 상태로 여러 도구를 등록할 수 있습니다.
export default function (pi: ExtensionAPI) {
let connection = null;
pi.registerTool({ name: "db_connect", ... });
pi.registerTool({ name: "db_query", ... });
pi.registerTool({ name: "db_close", ... });
pi.on("session_shutdown", async () => {
connection?.close();
});
}맞춤형 렌더링
도구는 사용자 정의 TUI 디스플레이를 위해 renderCall 및 renderResult를 제공할 수 있습니다. 전체 구성 요소는 tui.md를, 도구 행 구성 방법은 API를, tool-execution.ts를 참조하세요.
기본적으로 도구 출력은 패딩과 배경을 처리하는 Box로 래핑됩니다. 정의된 renderCall 또는 renderResult는 Component를 반환해야 합니다. 슬롯 렌더러가 정의되지 않은 경우 tool-execution.ts는 해당 슬롯에 대해 대체 렌더링을 사용합니다.
도구가 기본 Box를 사용하는 대신 자체 셸을 렌더링해야 하는 경우 renderShell: "self"를 설정하세요. 이는 프레임이나 배경 동작을 완벽하게 제어해야 하는 도구에 유용합니다. 예를 들어 도구가 안정된 후에도 시각적으로 안정적인 상태를 유지해야 하는 대규모 미리 보기입니다.
pi.registerTool({
name: "my_tool",
label: "My Tool",
description: "Custom shell example",
parameters: Type.Object({}),
renderShell: "self",
async execute() {
return { content: [{ type: "text", text: "ok" }], details: undefined };
},
renderCall(args, theme, context) {
return new Text(theme.fg("accent", "my custom shell"), 0, 0);
},
});renderCall 및 renderResult는 각각 다음을 포함하는 context 객체를 받습니다.
args- 현재 도구 호출 인수state-renderCall및renderResult에서 공유 행-로컬 상태lastComponent- 해당 슬롯에 대해 이전에 반환된 구성요소(있는 경우)invalidate()- 이 도구 행의 다시 렌더링을 요청합니다.toolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
교차 슬롯 공유 상태에는 context.state를 사용하세요. 렌더링 전체에서 동일한 구성 요소를 재사용하고 변경하려는 경우 반환된 구성 요소 인스턴스에 슬롯 로컬 캐시를 유지하세요.
렌더콜
도구 호출 또는 헤더를 렌더링합니다.
import { Text } from "@earendil-works/pi-tui";
renderCall(args, theme, context) {
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
let content = theme.fg("toolTitle", theme.bold("my_tool "));
content += theme.fg("muted", args.action);
if (args.text) {
content += " " + theme.fg("dim", `"${args.text}"`);
}
text.setText(content);
return text;
}렌더링 결과
도구 결과 또는 출력을 렌더링합니다.
renderResult(result, { expanded, isPartial }, theme, context) {
if (isPartial) {
return new Text(theme.fg("warning", "Processing..."), 0, 0);
}
if (result.details?.error) {
return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
}
let text = theme.fg("success", "✓ Done");
if (expanded && result.details?.items) {
for (const item of result.details.items) {
text += "\n " + theme.fg("dim", item);
}
}
return new Text(text, 0, 0);
}슬롯에 의도적으로 표시되는 콘텐츠가 없는 경우 빈 Container와 같은 빈 Component를 반환합니다.
키바인딩 힌트
활성 키 바인딩 구성을 존중하는 키 바인딩 힌트를 표시하려면 keyHint()를 사용하세요.
import { keyHint } from "@earendil-works/pi-coding-agent";
renderResult(result, { expanded }, theme, context) {
let text = theme.fg("success", "✓ Done");
if (!expanded) {
text += ` (${keyHint("app.tools.expand", "to expand")})`;
}
return new Text(text, 0, 0);
}사용 가능한 기능:
keyHint(keybinding, description)-"app.tools.expand"또는"tui.select.confirm"와 같이 구성된 키 바인딩 ID의 형식을 지정합니다.keyText(keybinding)- 키 바인딩 ID에 대해 구성된 원시 키 텍스트를 반환합니다.rawKeyHint(key, description)- 원시 키 문자열 형식 지정
네임스페이스가 있는 키 바인딩 ID를 사용하세요.
- 코딩 에이전트 ID는
app.*네임스페이스를 사용합니다(예:app.tools.expand,app.editor.external,app.session.rename). - 공유 TUI ID는
tui.*네임스페이스를 사용합니다(예:tui.select.confirm,tui.select.cancel,tui.input.tab).
키 바인딩 ID 및 기본값의 전체 목록은 keybindings.md를 참조하세요. keybindings.json는 동일한 네임스페이스 ID를 사용합니다.
사용자 정의 편집기와 ctx.ui.custom() 구성 요소는 삽입된 인수로 keybindings: KeybindingsManager를 받습니다. getKeybindings() 또는 setKeybindings()를 호출하는 대신 삽입된 관리자를 직접 사용해야 합니다.
모범 사례
Text를 패딩(0, 0)과 함께 사용하세요. 기본 Box는 패딩을 처리합니다.- 여러 줄로 구성된 콘텐츠에는
\n를 사용하세요. - 스트리밍 진행을 위해
isPartial를 처리합니다. - 자세한 내용은 요청 시
expanded로 문의하세요. - 기본 보기를 컴팩트하게 유지하세요.
- 인수를
context.state에 복사하는 대신renderResult에서context.args를 읽으세요. - 호출 및 결과 슬롯 전체에서 공유되어야 하는 데이터에만
context.state를 사용하세요. - 동일한 구성요소 인스턴스를 업데이트할 수 있는 경우
context.lastComponent를 재사용하세요. - 기본 박스형 셸이 방해가 되는 경우에만
renderShell: "self"를 사용하세요. 자체 셸 모드에서는 도구가 자체 프레임, 패딩 및 배경을 담당합니다.
대체
슬롯 렌더러가 정의되지 않았거나 발생하는 경우:
renderCall: 도구 이름을 표시합니다.renderResult:content의 원시 텍스트를 표시합니다.
동적 도구 로딩
Extensions 작은 초기 세트만 활성화하면서 많은 도구를 등록할 수 있습니다. 그런 다음 도구는 실행 중에 pi.setActiveTools()를 사용하여 더 많은 도구를 추가할 수 있습니다. Pi 순전히 추가된 변경 사항을 감지하고 해당 도구 결과에 새로 사용 가능한 도구 이름을 기록하며 다음 모델 요청 전에 업데이트된 활성 세트를 적용합니다.
이것은 모든 모델에서 작동합니다. Models 기본 지연 로딩 지원을 통해 안정적인 프롬프트 접두사를 유지하고 도구 결과 위치에 새 정의를 로드합니다. 다른 모델은 아래 설명된 대체 방법을 사용합니다.
수명주기는 다음과 같습니다
- 모든 도구를
pi.registerTool()로 등록하면pi.getAllTools()에 표시됩니다. search_tools와 같은 로더 도구는 활성 상태로 유지하고 검색 가능한 도구는 비활성 상태로 둡니다.- 로더 실행 중에
pi.setActiveTools([...currentTools,...matchingTools])를 호출하세요. 변경 사항은 추가되어야 합니다. 동일한 호출에서 현재 활성 도구를 제거하지 마십시오. - Pi 로더의 도구 결과에 어떤 도구가 추가되었는지 기록합니다.
- 다음 모델 응답 전에 Pi는 지원되는 경우 기본 지연 로딩을 사용하고 그렇지 않은 경우 일반 활성 도구 목록을 사용하여 추가된 정의를 노출합니다.
공급자별 도구 참조를 반환하거나 로더를 특수 검색 도구로 표시할 필요가 없습니다. 활성 공구 교환이 신호입니다. pi.setActiveTools()에 전달된 이름은 이미 등록되어 있어야 합니다. 알 수 없는 이름은 무시됩니다.
Models 기본 지연 로딩 포함
- 인류학
- Models: Sonnet, Opus, Fable 버전 4.5 이상(Haiku 제외)
- 기본 표현: 지연된 정의는
defer_loading를 사용합니다. 로드 포인트는tool_reference콘텐츠를 사용합니다.
- 오픈AI
- Models:
gpt-5.4이상 가족 - 기본 표현: Pi 로드 지점에 완성된 클라이언트
tool_search_call및tool_search_output항목을 추가합니다.
- Models:
검증된 사용자 정의 모델 또는 프록시의 경우 anthropic-messages의 경우 compat.supportsToolReferences: true, openai-responses 및 openai-codex-responses의 경우 compat.supportsToolSearch: true를 사용하여 기본 처리를 활성화할 수 있습니다. 엔드포인트와 모델이 해당 기본 프로토콜을 수락하지 않는 한 이를 비활성화된 상태로 둡니다.
대체 동작
다른 모든 모델 및 공급자의 경우 동적 활성화가 계속 작동합니다. Pi는 다음 요청 시 일반적으로 전체 현재 활성 도구 목록을 보냅니다. 모델은 새로 활성화된 도구를 호출할 수 있지만 해당 정의를 추가하면 공급자의 캐시된 프롬프트 접두사가 무효화될 수 있습니다.
Pi 또한 한 도구 그룹을 다른 도구 그룹으로 교체하는 등 활성 세트가 순전히 추가되지 않는 경우에도 이 안전한 대체를 사용합니다. 따라서 도구 제거는 작동하지만 지연 로드를 사용하지 않습니다.
최상의 캐시 동작을 위해서는 전체 세션 동안 로더 도구를 활성 상태로 유지하고 활성 세트를 교체하는 대신 도구를 추가하십시오. 또한 promptSnippet 또는 promptGuidelines를 사용하여 도구를 활성화하면 시스템 프롬프트가 다시 작성됩니다. 공급자가 지연된 스키마를 지원하는 경우에도 시스템 프롬프트 변경으로 인해 접두사가 무효화될 수 있습니다. 느리게 로드된 도구는 일반적으로 도구 description에 의존해야 하며 활성 전용 프롬프트 메타데이터를 생략해야 합니다.
검색 도구 예
다음 확장은 두 개의 검색 가능한 도구를 등록하고 초기 활성 세트에서 제거하며 search_tools만 로더로 유지합니다. 이 예에서는 간단한 키워드 일치를 사용하지만 검색 구현에서는 BM25, 임베딩, 원격 카탈로그 또는 프로젝트별 라우팅을 사용할 수 있습니다.
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "lookup_weather",
label: "Lookup Weather",
description: "Look up the current weather for a city",
parameters: Type.Object({ city: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
details: {},
};
},
});
pi.registerTool({
name: "search_issues",
label: "Search Issues",
description: "Search project issues by keyword",
parameters: Type.Object({ query: Type.String() }),
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `No open issues matching ${params.query}` }],
details: {},
};
},
});
pi.registerTool({
name: "search_tools",
label: "Search Tools",
description: "Search for and enable tools relevant to a task",
promptSnippet: "Search for additional tools when the active tools cannot perform the task",
promptGuidelines: [
"Use search_tools when a task requires a capability that is not currently available.",
],
parameters: Type.Object({
query: Type.String({ description: "Capability or task to search for" }),
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
}),
async execute(_toolCallId, params) {
const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
const matches = pi.getAllTools()
.filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
.map((tool) => ({
tool,
score: terms.reduce(
(score, term) =>
score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
0,
),
}))
.filter((match) => match.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, params.limit ?? 3)
.map((match) => match.tool.name);
if (matches.length === 0) {
return {
content: [{ type: "text", text: `No tools found for: ${params.query}` }],
details: { matches: [] },
};
}
const active = pi.getActiveTools();
const added = matches.filter((name) => !active.includes(name));
pi.setActiveTools([...new Set([...active, ...added])]);
return {
content: [{
type: "text",
text: added.length > 0
? `Loaded tools: ${added.join(", ")}`
: `Matching tools already active: ${matches.join(", ")}`,
}],
details: { matches, added },
};
},
});
pi.on("session_start", () => {
// Keep searchable tools registered but initially inactive. Preserve built-ins
// and tools owned by other extensions, and keep the loader itself active.
const initialTools = pi.getActiveTools().filter(
(name) => !SEARCHABLE_TOOL_NAMES.has(name),
);
pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
});
}search_tools가 일치 항목을 추가하면 모델은 바로 다음 요청에서 해당 정의를 받습니다. 기본 지원 모델에서 정의는 초기 도구 스키마 접두사를 변경하지 않고 검색 결과 뒤에 고정됩니다. 다른 모델에서는 동일한 후속 요청의 일반 도구 목록에 나타납니다.
커스텀 UI
Extensions는 ctx.ui 메소드를 통해 사용자와 상호작용하고 메시지/도구가 렌더링되는 방식을 맞춤설정할 수 있습니다.
**맞춤 구성요소의 경우 다음에 대한 복사-붙여넣기 패턴이 있는 tui.md**를 참조하세요.
- 선택 대화 상자(SelectList)
- 취소를 사용한 비동기 작업(BorderedLoader)
- 설정 토글(SettingsList)
- 상태 표시기(setStatus)
- 스트리밍 중 작동하는 메시지, 가시성 및 표시기(
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - 편집기 위/아래 위젯(setWidget)
- 내장된 슬래시/경로 완성 위에 계층화된 자동 완성 제공자(addAutocompleteProvider)
- 맞춤 바닥글(setFooter)
대화상자
// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);
// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");
// Text input
const name = await ctx.ui.input("Name:", "placeholder");
// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");
// Notification (non-blocking)
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"카운트다운이 포함된 시간 제한 대화
대화상자는 실시간 카운트다운 표시와 함께 자동으로 닫히는 timeout 옵션을 지원합니다.
// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ timeout: 5000 }
);
if (confirmed) {
// User confirmed
} else {
// User cancelled or timed out
}시간 초과 시 반환 값:
select()반환undefinedconfirm()반환falseinput()반환undefined
AbortSignal을 사용한 수동 해제
더 세밀하게 제어하려면(예: 시간 초과와 사용자 취소를 구별하려면) AbortSignal를 사용하세요.
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
const confirmed = await ctx.ui.confirm(
"Timed Confirmation",
"This dialog will auto-cancel in 5 seconds. Confirm?",
{ signal: controller.signal }
);
clearTimeout(timeoutId);
if (confirmed) {
// User confirmed
} else if (controller.signal.aborted) {
// Dialog timed out
} else {
// User cancelled (pressed Escape or selected "No")
}전체 예시는 examples/extensions/timed-confirm.ts를 참조하세요.
위젯, 상태 및 바닥글
// Status in footer (persistent until cleared)
ctx.ui.setStatus("my-ext", "Processing...");
ctx.ui.setStatus("my-ext", undefined); // Clear
// Working loader (shown during streaming)
ctx.ui.setWorkingMessage("Thinking deeply...");
ctx.ui.setWorkingMessage(); // Restore default
ctx.ui.setWorkingVisible(false); // Hide the built-in working loader row entirely
ctx.ui.setWorkingVisible(true); // Show the built-in working loader row
// Working indicator (shown during streaming)
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] }); // Static dot
ctx.ui.setWorkingIndicator({
frames: [
ctx.ui.theme.fg("dim", "·"),
ctx.ui.theme.fg("muted", "•"),
ctx.ui.theme.fg("accent", "●"),
ctx.ui.theme.fg("muted", "•"),
],
intervalMs: 120,
});
ctx.ui.setWorkingIndicator({ frames: [] }); // Hide indicator
ctx.ui.setWorkingIndicator(); // Restore default spinner
// Widget above editor (default)
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Widget below editor
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
ctx.ui.setWidget("my-widget", (tui, theme) => new Text(theme.fg("accent", "Custom"), 0, 0));
ctx.ui.setWidget("my-widget", undefined); // Clear
// Custom footer (replaces built-in footer entirely)
ctx.ui.setFooter((tui, theme) => ({
render(width) { return [theme.fg("dim", "Custom footer")]; },
invalidate() {},
}));
ctx.ui.setFooter(undefined); // Restore built-in footer
// Terminal title
ctx.ui.setTitle("pi - my-project");
// Editor text
ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
// Paste into editor (triggers paste handling, including collapse for large content)
ctx.ui.pasteToEditor("pasted content");
// Stack custom autocomplete behavior on top of the built-in provider
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, line, col, options) {
const beforeCursor = (lines[line] ?? "").slice(0, col);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, line, col, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [{ value: "#2983", label: "#2983", description: "Extension API for autocomplete" }],
};
},
applyCompletion(lines, line, col, item, prefix) {
return current.applyCompletion(lines, line, col, item, prefix);
},
shouldTriggerFileCompletion(lines, line, col) {
return current.shouldTriggerFileCompletion?.(lines, line, col) ?? true;
},
}));
// Tool output expansion
const wasExpanded = ctx.ui.getToolsExpanded();
ctx.ui.setToolsExpanded(true);
ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)
const themes = ctx.ui.getAllThemes(); // [{ name: "dark", path: "/..." | undefined }, ...]
const lightTheme = ctx.ui.getTheme("light"); // Load without switching
const result = ctx.ui.setTheme("light"); // Switch by name
if (!result.success) {
ctx.ui.notify(`Failed: ${result.error}`, "error");
}
ctx.ui.setTheme(lightTheme!); // Or switch by Theme object
ctx.ui.theme.fg("accent", "styled text"); // Access current theme사용자 정의 작업 표시기 프레임은 그대로 렌더링됩니다. 색상을 원하면 ctx.ui.theme.fg(...) 등을 사용하여 프레임 문자열에 직접 추가하세요.
자동완성 Providers
ctx.ui.addAutocompleteProvider()를 사용하여 내장된 슬래시 명령 및 경로 제공자 위에 사용자 정의 자동 완성 논리를 쌓습니다. ``ctx.ui.addAutocompleteProvider()를 사용하여 내장된 슬래시 명령 및 경로 제공자 위에 사용자 정의 자동 완성 논리를 쌓습니다. 와 같은 사용자 지정 자연 트리거의 경우 triggerCharacters`를 설정합니다.
일반적인 패턴:
- 커서 앞의 텍스트를 검사합니다.
- 확장 관련 구문이 일치하면 자체 제안을 반환합니다.
- 그렇지 않으면
current.getSuggestions(...)에 위임하세요 - 사용자 정의 삽입 동작이 필요하지 않은 경우 위임
applyCompletion(...)
pi.on("session_start", (_event, ctx) => {
ctx.ui.addAutocompleteProvider((current) => ({
triggerCharacters: ["#"],
async getSuggestions(lines, cursorLine, cursorCol, options) {
const line = lines[cursorLine] ?? "";
const beforeCursor = line.slice(0, cursorCol);
const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
if (!match) {
return current.getSuggestions(lines, cursorLine, cursorCol, options);
}
return {
prefix: `#${match[1] ?? ""}`,
items: [
{ value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
{ value: "#2753", label: "#2753", description: "Reload stale resource settings" },
],
};
},
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
},
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
},
}));
});gh issue list로 최신 공개 GitHub 이슈를 미리 로드하고 빠른 #... 완료를 위해 로컬로 필터링하는 전체 예제는 github-issue-autocomplete.ts를 참조하세요. GitHub CLI (gh) 및 GitHub 저장소 체크아웃이 필요합니다.
맞춤 구성요소
복잡한 UI의 경우 ctx.ui.custom()를 사용하세요. done()가 호출될 때까지 일시적으로 편집기가 구성 요소로 대체됩니다.
import { Text, Component } from "@earendil-works/pi-tui";
const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);
text.onKey = (key) => {
if (key === "return") done(true);
if (key === "escape") done(false);
return true;
};
return text;
});
if (result) {
// User pressed Enter
}콜백은 다음을 수신합니다.
tui- TUI 인스턴스 (화면 크기, 포커스 관리용)theme- 스타일링을 위한 현재 테마keybindings- 앱 키바인딩 관리자(단축키 확인용)done(value)- 구성 요소를 닫고 값을 반환하는 호출
전체 구성요소 API를 보려면 tui.md를 참조하세요.
오버레이 모드(실험적)
화면을 지우지 않고 기존 콘텐츠 위에 부동 모달로 구성 요소를 렌더링하려면 { overlay: true }를 전달합니다.
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);고급 위치 지정(앵커, 여백, 백분율, 반응형 가시성)의 경우 overlayOptions를 전달하세요. onHandle를 사용하여 프로그래밍 방식으로 초점이나 가시성을 제어합니다.
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{
overlay: true,
overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
onHandle: (handle) => {
handle.focus(); // focus this overlay and bring it to the visual front
// handle.unfocus({ target: editorComponent }); // release input to a specific component
// handle.setHidden(true/false); // toggle visibility
// handle.hide(); // permanently remove
}
}
);집중된 표시 오버레이는 오버레이가 아닌 임시 사용자 정의 UI가 닫힌 후 입력을 회수할 수 있습니다. 오버레이가 계속 표시되는 동안 의도적으로 다른 구성요소가 입력을 유지하도록 하려면 handle.unfocus({ target })를 호출하세요. { target: null }를 전달하면 다른 구성요소에 초점을 맞추지 않고 오버레이가 해제됩니다.
전체 OverlayOptions는 tui.md를, 예시는 OverlayHandle API 및 overlay-qa-tests.ts를 참조하세요.
맞춤 편집기
기본 입력 편집기를 사용자 정의 구현(vim 모드, emacs 모드 등)으로 바꿉니다.
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";
class VimEditor extends CustomEditor {
private mode: "normal" | "insert" = "insert";
handleInput(data: string): void {
if (matchesKey(data, "escape") && this.mode === "insert") {
this.mode = "normal";
return;
}
if (this.mode === "normal" && data === "i") {
this.mode = "insert";
return;
}
super.handleInput(data); // App keybindings + text editing
}
}
export default function (pi: ExtensionAPI) {
pi.on("session_start", (_event, ctx) => {
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new VimEditor(tui, theme, keybindings)
);
});
}핵심 사항:
CustomEditor(기본Editor아님)을 확장하여 앱 키 바인딩을 얻습니다(중단하려면 이스케이프, Ctrl+D, 모델 전환).- 처리할 수 없는 키는
super.handleInput(data)로 전화하세요. - 공장은 앱에서
tui,theme,keybindings를 받습니다. - 이전에 구성된 사용자 정의 편집기를 래핑하려면
setEditorComponent()앞에ctx.ui.getEditorComponent()를 사용하세요. - 기본값을 복원하려면
undefined를 전달하세요.ctx.ui.setEditorComponent(undefined)
이미 편집기를 대체한 다른 확장으로 작성하려면 설정하기 전에 이전 팩토리를 캡처하세요.
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);모드 표시기가 포함된 전체 예는 tui.md 패턴 7을 참조하세요.
메시지 및 항목 렌더링
customType를 사용하여 메시지용 사용자 정의 렌더러를 등록하세요. LLM 컨텍스트에 참여해야 하는 콘텐츠에 대해 메시지 렌더러를 사용합니다.
import { Text } from "@earendil-works/pi-tui";
pi.registerMessageRenderer("my-extension", (message, options, theme) => {
const { expanded, outputPad } = options;
let text = theme.fg("accent", `[${message.customType}] `);
text += message.content;
if (expanded && message.details) {
text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
}
return new Text(text, outputPad, 0);
});메시지는 pi.sendMessage()를 통해 전송됩니다.
pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});LLM으로 보내면 안 되는 TUI 전용 콘텐츠의 경우 대신 맞춤 항목을 렌더링하세요.
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });테마 색상
모든 렌더링 함수는 theme 객체를 받습니다. 사용자 정의 테마 및 전체 색상 팔레트를 생성하려면 themes.md를 참조하세요.
// Foreground colors
theme.fg("toolTitle", text) // Tool names
theme.fg("accent", text) // Highlights
theme.fg("success", text) // Success (green)
theme.fg("error", text) // Errors (red)
theme.fg("warning", text) // Warnings (yellow)
theme.fg("muted", text) // Secondary text
theme.fg("dim", text) // Tertiary text
// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)사용자 정의 도구 렌더러의 구문 강조 표시:
import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";
// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);
// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs"); // "rust"
const highlighted = highlightCode(code, lang, theme);오류 처리
- 확장 프로그램 오류가 기록되고 에이전트가 계속됩니다.
tool_call오류로 인해 도구가 차단됩니다(안전 장치).- 도구
execute오류는 던짐으로 표시되어야 합니다. 발생한 오류가 포착되어isError: true로 LLM에 보고되고 실행이 계속됩니다.
모드 동작
| 방법 | ctx.mode |
ctx.hasUI |
메모 |
|---|---|---|---|
| 인터랙티브 | "tui" |
true |
터미널 렌더링으로 전체 TUI |
RPC (--mode rpc) |
"rpc" |
true |
JSON 프로토콜을 통한 대화상자 및 알림 custom()는 undefined를 반환합니다. rpc.md 참조 |
JSON (--mode json) |
"json" |
false |
이벤트 스트림을 stdout로; UI 메서드는 작동하지 않습니다. |
인쇄(-p) |
"print" |
false |
Extensions 실행했지만 메시지를 표시할 수 없음 |
TUI 특정 기능(custom(), 컴포넌트 팩토리, 터미널 입력) 이전에는 ctx.mode === "tui"을 사용하세요. TUI 및 RPC 모드 모두에서 작동하는 대화 상자 및 알림 방법 전에 ctx.hasUI를 사용하세요.
예시 참조
examples/extensions/의 모든 예시.
| 예 | 설명 | 키 API |
|---|---|---|
| 도구 | ||
hello.ts |
최소한의 도구 등록 | registerTool |
question.ts |
사용자 상호작용이 가능한 도구 | registerTool, ui.select |
questionnaire.ts |
다단계 마법사 도구 | registerTool, ui.custom |
todo.ts |
지속성을 갖춘 상태 저장 도구 | registerTool, appendEntry, renderResult, 세션 이벤트 |
dynamic-tools.ts |
시작 후 및 명령 중에 도구 등록 | registerTool, session_start, registerCommand |
structured-output.ts |
terminate: true를 사용한 최종 구조화된 출력 도구 |
registerTool, 도구 결과 종료 |
truncated-tool.ts |
출력 잘림 예 | registerTool, truncateHead |
tool-override.ts |
내장된 읽기 도구 재정의 | registerTool (내장된 이름과 동일) |
| 명령 | ||
pirate.ts |
턴당 시스템 프롬프트 수정 | registerCommand, before_agent_start |
summarize.ts |
대화 요약 명령 | registerCommand, ui.custom |
handoff.ts |
공급자 간 모델 핸드오프 | registerCommand, ui.editor, ui.custom |
qna.ts |
맞춤형 UI에 대한 Q&A | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
사용자 메시지 삽입 | registerCommand, sendUserMessage |
reload-runtime.ts |
다시 로드 명령 및 LLM 도구 전달 | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
정상적인 종료 명령 | registerCommand, shutdown() |
| 이벤트 및 게이트 | ||
permission-gate.ts |
위험한 명령 차단 | on("tool_call"), ui.confirm |
project-trust.ts |
사용자/전역 또는 CLI 확장자로부터 프로젝트 신뢰를 결정하거나 연기합니다. | on("project_trust"), 신뢰 UI, 필수 신뢰 결과 |
protected-paths.ts |
특정 경로에 대한 쓰기 차단 | on("tool_call") |
confirm-destructive.ts |
세션 변경 사항 확인 | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
더러운 git repo에 대해 경고 | on("session_before_*"), exec |
input-transform.ts |
사용자 입력 변환 | on("input") |
input-transform-streaming.ts |
스트리밍 인식 입력 변환 | on("input"), streamingBehavior |
model-status.ts |
React 모델 변경 | on("model_select"), setStatus |
provider-payload.ts |
페이로드 및 공급자 응답 헤더 검사 | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
시스템 프롬프트 정보 표시 | on("agent_start"), getSystemPrompt |
claude-rules.ts |
파일에서 규칙 로드 | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
systemPromptOptions를 사용하여 상황 인식 도구 안내 추가 |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
파일 감시자가 메시지를 트리거합니다. | sendMessage |
| 압축 및 세션 | ||
custom-compaction.ts |
사용자 정의 압축 요약 | on("session_before_compact") |
trigger-compact.ts |
압축을 수동으로 트리거 | compact() |
git-checkpoint.ts |
Git 턴에 보관함 | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
충돌 가져오기, 병합 및 해결 | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
종료 시 커밋 | on("session_shutdown"), exec |
| UI 구성요소 | ||
status-line.ts |
바닥글 상태 표시기 | setStatus, 세션 이벤트 |
working-indicator.ts |
스트리밍 작업 표시기 사용자 정의 | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
gh issue list에서 최근 열린 이슈를 미리 로드하여 내장된 자동 완성 위에 #1234 이슈 완료를 추가하세요. |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
바닥글 전체 바꾸기 | registerCommand, setFooter |
custom-header.ts |
시작 헤더 교체 | on("session_start"), setHeader |
modal-editor.ts |
Vim 스타일 모달 편집기 | setEditorComponent, CustomEditor |
rainbow-editor.ts |
사용자 정의 편집기 스타일링 | setEditorComponent |
widget-placement.ts |
편집기 위/아래 위젯 | setWidget |
overlay-test.ts |
오버레이 구성요소 | ui.custom 오버레이 옵션 포함 |
overlay-qa-tests.ts |
포괄적인 오버레이 테스트 | ui.custom, 모든 오버레이 옵션 |
notify.ts |
간단한 알림 | ui.notify |
timed-confirm.ts |
시간 초과가 있는 대화 상자 | ui.confirm 시간 초과/신호 포함 |
mac-system-theme.ts |
테마 자동 전환 | setTheme, exec |
| 복잡함 Extensions | ||
plan-mode/ |
전체 계획 모드 구현 | 모든 이벤트 유형, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
저장 가능한 사전 설정(모델, 도구, 사고) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
도구 UI 켜기/끄기 전환 | registerCommand, setActiveTools, SettingsList, 세션 이벤트 |
| 원격 및 샌드박스 | ||
ssh.ts |
SSH 원격 실행 | registerFlag, on("user_bash"), on("before_agent_start"), 도구 작업 |
interactive-shell.ts |
영구 셸 세션 | on("user_bash") |
sandbox/ |
샌드박스 도구 실행 | 도구 작업 |
gondolin/ |
내장 도구와 ! 명령을 Gondolin 마이크로 VM으로 라우팅 |
도구 작업, 내장 도구 재정의, on("user_bash") |
subagent/ |
하위 에이전트 생성 | registerTool, exec |
| 계략 | ||
snake.ts |
뱀 게임 | registerCommand, ui.custom, 키보드 처리 |
space-invaders.ts |
스페이스 인베이더 게임 | registerCommand, ui.custom |
doom-overlay/ |
오버레이의 파멸 | ui.custom 오버레이 포함 |
| Providers | ||
custom-provider-anthropic/ |
맞춤형 인류 프록시 | registerProvider |
custom-provider-gitlab-duo/ |
GitLab Duo 통합 | registerProvider와 OAuth |
| 메시지 및 커뮤니케이션 | ||
message-renderer.ts |
사용자 정의 메시지 렌더링 | registerMessageRenderer, sendMessage |
entry-renderer.ts |
TUI 전용 사용자 정의 항목 렌더링 | registerEntryRenderer, appendEntry |
event-bus.ts |
확장 간 이벤트 | pi.events |
| 세션 메타데이터 | ||
session-name.ts |
선택자의 세션 이름 지정 | setSessionName, getSessionName |
bookmark.ts |
/tree에 대한 북마크 항목 | setLabel |
| 기타 | ||
inline-bash.ts |
도구 호출의 인라인 bash | on("tool_call") |
bash-spawn-hook.ts |
실행 전 bash 명령어, cwd, env 조정 | createBashTool, spawnHook |
with-deps/ |
npm 종속성이 있는 확장 | package.json를 사용한 패키지 구조 |