Extensions
pi dapat membuat ekstensi. Mintalah untuk membuat satu untuk kasus penggunaan Anda.
Extensions adalah modul TypeScript yang memperluas perilaku pi. Mereka dapat berlangganan peristiwa siklus hidup, mendaftarkan alat khusus yang dapat dipanggil oleh LLM, menambahkan perintah, dan banyak lagi.
Penempatan untuk /muat ulang: Masukkan ekstensi di
~/.pi/agent/extensions/(global) atau.pi/extensions/(proyek-lokal) untuk penemuan otomatis. Gunakanpi -e./path.tshanya untuk tes cepat. Extensions di lokasi yang ditemukan secara otomatis dapat diisi ulang dengan/reload.
Kemampuan utama:
- Alat khusus - Daftarkan alat yang dapat dihubungi LLM melalui
pi.registerTool() - Intersepsi peristiwa - Memblokir atau mengubah panggilan alat, memasukkan konteks, menyesuaikan pemadatan
- Interaksi pengguna - Meminta pengguna melalui
ctx.ui(pilih, konfirmasi, masukkan, beri tahu) - Komponen UI khusus - Komponen TUI lengkap dengan input keyboard melalui
ctx.ui.custom()untuk interaksi kompleks - Perintah khusus - Daftarkan perintah seperti
/mycommandmelaluipi.registerCommand() - Kegigihan sesi - Status penyimpanan yang bertahan saat dimulai ulang melalui
pi.appendEntry() - Render khusus - Kontrol bagaimana panggilan alat/hasil dan pesan muncul di TUI
Contoh kasus penggunaan:
- Gerbang izin (konfirmasi sebelum
rm -rf,sudo, dll.) - Git pos pemeriksaan (simpanan di setiap belokan, pulihkan di cabang)
- Perlindungan jalur (blok tulis ke
.env,node_modules/) - Pemadatan khusus (ringkas percakapan sesuai keinginan Anda)
- Ringkasan percakapan (lihat contoh
summarize.ts) - Alat interaktif (pertanyaan, penyihir, dialog khusus)
- Alat stateful (daftar tugas, kumpulan koneksi)
- Integrasi eksternal (pengamat file, webhook, pemicu CI)
- Permainan sambil menunggu (lihat contoh
snake.ts)
Lihat examples/extensions/ untuk implementasi kerja.
Daftar isi
- 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
Mulai Cepat
Buat ~/.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");
},
});
}Uji dengan tanda --extension (atau -e):
pi -e ./my-extension.tsLokasi Perluasan
Keamanan: Extensions dijalankan dengan izin sistem penuh Anda dan dapat mengeksekusi kode arbitrer. Instal hanya dari sumber yang Anda percayai.
Extensions ditemukan secara otomatis dari lokasi tepercaya. Entri proyek-lokal .pi/extensions dimuat hanya setelah proyek dipercaya.
| Lokasi | Cakupan |
|---|---|
~/.pi/agent/extensions/*.ts |
Global (semua proyek) |
~/.pi/agent/extensions/*/index.ts |
Global (subdirektori) |
.pi/extensions/*.ts |
Proyek-lokal |
.pi/extensions/*/index.ts |
Proyek-lokal (subdirektori) |
Jalur tambahan melalui 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"
]
}Untuk berbagi ekstensi melalui npm atau paket git sebagai pi, lihat packages.md.
Impor yang Tersedia
| Kemasan | Tujuan |
|---|---|
@earendil-works/pi-coding-agent |
Jenis ekstensi (ExtensionAPI, ExtensionContext, acara) |
typebox |
Definisi skema untuk parameter alat |
@earendil-works/pi-ai |
Utilitas AI (StringEnum untuk enum yang kompatibel dengan Google) |
@earendil-works/pi-tui |
TUI komponen untuk rendering khusus |
npm dependensi juga berfungsi. Tambahkan package.json di sebelah ekstensi Anda (atau di direktori induk), jalankan npm install, dan impor dari node_modules/ diselesaikan secara otomatis.
Untuk paket pi terdistribusi yang diinstal dengan pi install (npm atau git), deps runtime harus dalam dependencies. Instalasi paket menggunakan instalasi produksi (npm install --omit=dev) secara default, jadi devDependencies tidak tersedia saat runtime; ketika npmCommand dikonfigurasi, paket git menggunakan install biasa untuk kompatibilitas dengan pembungkus.
Node.js bawaan (node:fs, node:path, dll.) juga tersedia.
Menulis Ekstensi
Ekstensi mengekspor fungsi default pabrik yang menerima ExtensionAPI. Pabrik bisa sinkron atau asinkron:
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 dimuat melalui jiti, jadi TypeScript berfungsi tanpa kompilasi.
Jika pabrik mengembalikan Promise, pi menunggunya sebelum melanjutkan startup. Itu berarti inisialisasi asinkron selesai sebelum session_start, sebelum resources_discover, dan sebelum pendaftaran penyedia yang diantri melalui pi.registerProvider() dihapus.
Fungsi pabrik asinkron
Gunakan pabrik async untuk pekerjaan startup satu kali seperti mengambil konfigurasi jarak jauh atau menemukan model yang tersedia secara dinamis.
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,
})),
});
}Pola ini membuat model yang diambil tersedia selama pengaktifan normal dan hingga pi --list-models.
Sumber daya berumur panjang dan penutupan
Pabrik ekstensi dapat berjalan dalam pemanggilan yang tidak pernah memulai sesi. Jangan memulai sumber daya latar belakang seperti proses, soket, pengamat file, atau pengatur waktu dari pabrik.
Tunda pengaktifan sumber daya latar belakang hingga session_start atau perintah/alat/peristiwa yang memerlukan sumber daya. Daftarkan penangan session_shutdown idempoten untuk menutup sumber daya cakupan sesi apa pun yang Anda mulai.
Gaya Ekstensi
File tunggal - paling sederhana, untuk ekstensi kecil:
~/.pi/agent/extensions/
└── my-extension.tsDirektori dengan index.ts - untuk ekstensi multi-file:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper modulePaket dengan dependensi - untuk ekstensi yang memerlukan npm paket:
~/.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"]
}
}Jalankan npm install di direktori ekstensi, lalu impor dari node_modules/ berfungsi secara otomatis.
Acara
Ikhtisar Siklus Hidup
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_shutdownAcara Permulaan
proyek_kepercayaan
Diaktifkan sebelum pi memutuskan apakah akan mempercayai proyek dengan konfigurasi dinamis (.pi atau .agents/skills). Ini berjalan saat startup dan ketika penggantian sesi (misalnya /resume) memasuki cwd yang kepercayaannya belum terselesaikan dalam proses saat ini. Hanya ekstensi pengguna/global dan ekstensi CLI -e yang berpartisipasi; ekstensi proyek-lokal tidak dimuat sampai kepercayaan teratasi.
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" };
});Penangan project_trust harus mengembalikan { trusted: "yes" | "no" | "undecided" }. Ekstensi pengguna/global atau CLI yang mengembalikan "yes" atau "no" memiliki keputusan; keputusan ya/tidak yang pertama menang dan menekan perintah kepercayaan yang ada di dalamnya. Gunakan remember: true untuk mempertahankan keputusan ya/tidak; jika tidak, ini hanya berlaku untuk proses saat ini. Kembalikan "undecided" agar penangan selanjutnya atau aliran kepercayaan bawaan dapat memutuskan. Periksa ctx.hasUI sebelum meminta. Jika tidak ada pengendali yang mengembalikan ya/tidak, resolusi kepercayaan normal berlanjut: keputusan trust.json yang disimpan diterapkan terlebih dahulu, lalu defaultProjectTrust mengontrol apakah pi bertanya, memercayai, atau menolak secara default.
Peristiwa Sumber Daya
sumber daya_temukan
Diaktifkan setelah session_start sehingga ekstensi dapat menyumbangkan keahlian tambahan, prompt, dan jalur tema.
Jalur startup menggunakan reason: "startup". Muat ulang menggunakan 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"],
};
});Acara Sesi
Lihat Session Format untuk penyimpanan sesi internal dan SessionManager API.
sesi_mulai
Diaktifkan saat sesi dimulai, dimuat, atau dimuat ulang.
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");
});sesi_info_berubah
Diaktifkan ketika nama tampilan sesi saat ini diatur melalui /name, RPC, atau 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");
});sesi_sebelum_beralih
Diaktifkan sebelum memulai sesi baru (/new) atau berpindah sesi (/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 };
}
});Setelah tindakan peralihan atau sesi baru berhasil, pi mengeluarkan session_shutdown untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan session_start dengan reason: "new" | "resume" dan previousSessionFile.
Lakukan pekerjaan pembersihan di session_shutdown, lalu bangun kembali status dalam memori di session_start.
sesi_sebelum_fork
Dipecat saat melakukan forking melalui /fork atau mengkloning melalui /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
});Setelah fork atau kloning berhasil, pi mengeluarkan session_shutdown untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan session_start dengan reason: "fork" dan previousSessionFile.
Lakukan pekerjaan pembersihan di session_shutdown, lalu bangun kembali status dalam memori di session_start.
session_before_compact / session_compact
Ditembak saat pemadatan. Lihat compaction.md untuk detailnya.
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 / session_tree
Ditembak pada navigasi /tree. Lihat Sessions untuk konsep navigasi pohon.
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
});sesi_shutdown
Diaktifkan sebelum runtime sesi yang dimulai dirobohkan. Gunakan ini untuk membersihkan sumber daya yang dibuka dari session_start atau kait cakupan sesi lainnya.
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.
});Acara Agen
sebelum_agen_mulai
Dipecat setelah pengguna mengirimkan prompt, sebelum loop agen. Dapat memasukkan pesan dan/atau memodifikasi prompt sistem.
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...",
};
});Bidang systemPromptOptions memberikan ekstensi akses ke data terstruktur yang sama yang digunakan Pi untuk membuat perintah sistem. Hal ini memungkinkan Anda memeriksa apa yang Pi telah dimuat — perintah khusus, pedoman, cuplikan alat, context files, keterampilan — tanpa menemukan kembali sumber daya atau menguraikan ulang tanda. Gunakan saat ekstensi Anda perlu membuat perubahan mendalam dan terinformasi pada perintah sistem dengan tetap menghormati konfigurasi yang disediakan pengguna.
Di dalam before_agent_start, event.systemPrompt dan ctx.getSystemPrompt() keduanya mencerminkan perintah sistem berantai pada pengendali saat ini. Nanti before_agent_start penangan masih bisa memodifikasinya lagi.
agen_mulai / agen_end / agen_settled
agent_start terpicu saat proses agen tingkat rendah dimulai. agent_end terpicu saat proses tersebut berakhir, namun Pi masih dapat mencoba ulang secara otomatis, memadatkan otomatis, dan mencoba lagi, atau melanjutkan dengan pesan tindak lanjut yang antri. Gunakan agent_settled untuk integrasi status yang perlu diketahui Pi tidak akan terus berjalan secara otomatis.
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.
});turn_start / turn_end
Dipecat untuk setiap giliran (satu respons LLM + panggilan alat).
pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});pesan_mulai / pembaruan_pesan / pesan_akhir
Diaktifkan karena pembaruan siklus hidup pesan.
message_startdanmessage_enddiaktifkan untuk pesan pengguna, asisten, dan toolResult.message_updatediaktifkan untuk pembaruan streaming asisten.message_endpenangan dapat mengembalikan{ message }untuk menggantikan pesan yang telah diselesaikan. Penggantinya harus tetap samarole.
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
Diaktifkan karena pembaruan siklus hidup eksekusi alat.
Dalam mode alat paralel:
tool_execution_startdipancarkan dalam urutan sumber asisten selama fase pra-penerbangantool_execution_updateperistiwa mungkin disisipkan di seluruh alattool_execution_enddikeluarkan dalam urutan penyelesaian alat setelah setiap alat diselesaikan- peristiwa pesan
toolResultterakhir masih dipancarkan kemudian dalam urutan sumber asisten
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
});konteks
Dipecat sebelum setiap panggilan LLM. Ubah pesan secara non-destruktif. Lihat Session Format untuk jenis pesan.
pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});sebelum_penyedia_header
Diaktifkan setelah header HTTP keluar dipasang. Gunakan untuk menambah, mengganti, atau menghapus header permintaan.
Penangan bermutasi event.headers di tempatnya. Tetapkan kunci pada string untuk menambah atau menggantinya, atau ke null untuk menghapusnya.
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;
});Berjalan sekali per permintaan penyedia; percobaan ulang menggunakan kembali header yang sama daripada menembakkan kembali hook.
sebelum_penyedia_permintaan
Diaktifkan setelah payload khusus penyedia dibuat, tepat sebelum permintaan dikirim. Penangan dijalankan dalam urutan pemuatan ekstensi. Mengembalikan undefined membuat payload tidak berubah. Mengembalikan nilai lain akan menggantikan payload untuk penangan selanjutnya dan untuk permintaan sebenarnya.
Kait ini dapat menulis ulang instruksi sistem tingkat penyedia atau menghapusnya seluruhnya. Perubahan tingkat muatan tersebut tidak tercermin oleh ctx.getSystemPrompt(), yang melaporkan string perintah sistem Pi dan bukan muatan penyedia serial akhir.
pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});Ini terutama berguna untuk men-debug serialisasi penyedia dan perilaku cache.
after_provider_response
Diaktifkan setelah respons HTTP diterima dan sebelum isi alirannya digunakan. Penangan dijalankan dalam urutan pemuatan ekstensi.
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"]);
}
});Ketersediaan header tergantung pada penyedia dan transportasi. Providers bahwa respons HTTP abstrak tidak boleh mengekspos header.
Acara Model
model_pilih
Diaktifkan ketika model berubah melalui perintah /model, perputaran model (Ctrl+P), atau pemulihan sesi.
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");
});Gunakan ini untuk memperbarui elemen UI (bilah status, footer) atau melakukan inisialisasi khusus model saat model aktif berubah.
berpikir_tingkat_pilih
Dipecat ketika tingkat berpikir berubah. Ini hanya untuk pemberitahuan; nilai pengembalian handler diabaikan.
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}`);
});Gunakan ini untuk memperbarui UI ekstensi ketika pi.setThinkingLevel(), perubahan model, atau kontrol tingkat berpikir bawaan mengubah tingkat berpikir aktif.
Acara Alat
alat_panggilan
Diaktifkan setelah tool_execution_start, sebelum alat dijalankan. Dapat memblokir. Gunakan isToolCallEventType untuk mempersempit dan mendapatkan masukan yang diketik.
Sebelum tool_call berjalan, pi menunggu peristiwa Agen yang dipancarkan sebelumnya selesai dikuras melalui AgentSession. Ini berarti ctx.sessionManager diperbarui melalui pesan pemanggil alat asisten saat ini.
Dalam mode eksekusi alat paralel default, panggilan alat saudara dari pesan asisten yang sama dipra-penerbangan secara berurutan, lalu dieksekusi secara bersamaan. tool_call tidak dijamin melihat hasil alat saudara dari pesan asisten yang sama di ctx.sessionManager.
event.input bisa berubah. Mutasi di tempatnya untuk menambal argumen alat sebelum dieksekusi.
Jaminan perilaku:
- Mutasi ke
event.inputmempengaruhi eksekusi alat sebenarnya - Penangan
tool_callkemudian melihat mutasi yang dilakukan oleh penangan sebelumnya - Tidak ada validasi ulang yang dilakukan setelah mutasi Anda
- Kembalikan nilai dari
tool_callpemblokiran kontrol melalui{ block: true, reason?: string, terminate?: boolean } terminatehanya berlaku untuk panggilan yang diblokir; agen berhenti lebih awal hanya ketika setiap hasil akhir dalam batch dihentikan
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}`);
}
});Mengetik masukan alat khusus
Alat khusus harus mengekspor jenis masukannya:
// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;Gunakan isToolCallEventType dengan parameter tipe eksplisit:
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
}
});alat_hasil
Diaktifkan setelah eksekusi alat selesai dan sebelum tool_execution_end ditambah peristiwa pesan hasil alat akhir dikeluarkan. Dapat mengubah hasil.
Dalam mode pahat paralel, tool_result dan tool_execution_end dapat disisipkan dalam urutan penyelesaian pahat, sedangkan kejadian pesan akhir toolResult masih dikirimkan kemudian dalam urutan sumber asisten.
tool_result rantai penangan seperti middleware:
- Penangan dijalankan dalam urutan pemuatan ekstensi
- Setiap penangan melihat hasil terbaru setelah penangan sebelumnya berubah
- Penangan dapat mengembalikan sebagian patch (
content,details,isError, atauusage); bidang yang dihilangkan mempertahankan nilainya saat ini
Gunakan ctx.signal untuk pekerjaan asinkron bersarang di dalam pengendali. Hal ini memungkinkan Esc membatalkan panggilan model, fetch(), dan operasi sadar pembatalan lainnya yang dimulai oleh ekstensi.
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 };
});Acara Pesta Pengguna
pengguna_bash
Diaktifkan ketika pengguna menjalankan perintah ! atau !!. Dapat mencegat.
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 } };
});Masukan Acara
masukan
Dipicu ketika masukan pengguna diterima, setelah perintah ekstensi diperiksa tetapi sebelum perluasan keterampilan dan templat. Acara ini melihat teks masukan mentah, jadi /skill:foo dan /template belum diperluas.
Pemrosesan pesanan:
- Perintah ekstensi (
/cmd) diperiksa terlebih dahulu - jika ditemukan, handler dijalankan dan event input dilewati inputperistiwa kebakaran - dapat mencegat, mengubah, atau menangani- Jika tidak ditangani: perintah keterampilan (
/skill:name) diperluas ke konten keterampilan - Jika tidak ditangani: prompt templates (
/template) diperluas ke konten templat - Pemrosesan agen dimulai (
before_agent_start, dll.)
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
});Hasil:
continue- melewati tanpa perubahan (default jika handler tidak mengembalikan apa pun)transform- ubah teks/gambar, lalu lanjutkan perluasanhandled- lewati agen sepenuhnya (penangan pertama yang mengembalikan ini menang)
Mengubah rantai di seluruh penangan. Lihat input-transform.ts dan input-transform-streaming.ts untuk perutean streamingBehavior.
Konteks Ekstensi
Semua penangan menerima ctx: ExtensionContext.
ctx.ui
Metode UI untuk interaksi pengguna. Lihat Custom UI untuk detail selengkapnya.
ctx.mode
Mode lari saat ini: "tui", "rpc", "json", atau "print". Gunakan ctx.mode === "tui" untuk menjaga fitur khusus terminal seperti custom(), pabrik komponen, input terminal, dan rendering TUI langsung.
ctx.hasUI
true dalam mode TUI dan RPC. false dalam mode cetak (-p) dan mode JSON. Gunakan ini untuk menjaga metode dialog (select, confirm, input, editor) dan metode api-dan-lupakan (notify, setStatus, setWidget, setTitle, setEditorText) yang berfungsi baik di TUI maupun RPC mode. Dalam mode RPC, beberapa metode khusus TUI tidak dapat dioperasikan atau dikembalikan secara default (lihat rpc.md).
ctx.cwd
Direktori kerja saat ini.
Gunakan CONFIG_DIR_NAME alih-alih melakukan hardcoding .pi saat membuat jalur konfigurasi proyek-lokal. Distribusi yang diganti mereknya dapat menggunakan nama direktori konfigurasi yang berbeda.
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()
Mengembalikan apakah kepercayaan proyek-lokal aktif untuk konteks sesi saat ini. Hal ini mencakup keputusan perwalian sementara dan CLI pengesampingan perwalian, bukan hanya keputusan yang disimpan dalam penyimpanan perwalian global.
Gunakan ini sebelum membaca konfigurasi ekstensi proyek-lokal yang hanya berlaku untuk proyek tepercaya.
ctx.sessionManager
Akses hanya baca ke status sesi. Lihat Session Format untuk SessionManager API lengkap dan tipe entri.
Untuk tool_call, status ini disinkronkan melalui pesan asisten saat ini sebelum penangan dijalankan. Dalam mode eksekusi alat paralel, masih belum ada jaminan untuk menyertakan hasil alat saudara dari pesan asisten yang sama.
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
Akses ke model, penyedia, dan autentikasi terselesaikan. ctx.modelRegistry.getProvider(id) mengembalikan penyedia pi-ai yang efektif, sementara getProviderAuth(id) menyelesaikan API key saat ini, header, URL dasar, dan lingkungan cakupan penyedia tanpa memerlukan model yang dimuat. ctx.model adalah model aktif, dan ctx.thinkingLevel adalah tingkat berpikir efektif saat ini.
ctx.scopedModels adalah daftar model baca-saja yang tercakup dalam sesi saat ini — kumpulan yang sama yang ditampilkan oleh perintah /scoped-models. Ini diselesaikan pada awal sesi dari bendera --models CLI dan pengaturan enabledModels (dicocokkan dengan katalog yang tersedia dengan minimatch di provider/modelId atau modelId kosong). Ini kosong jika tidak ada pelingkupan yang dikonfigurasi, artinya setiap model yang tersedia dapat digunakan. Setiap entri adalah { model, thinkingLevel? }, dengan thinkingLevel diatur hanya ketika pola menyematkannya (misalnya anthropic/*:high). Gunakan ini untuk mengisi pemilih model yang mencerminkan pemilih model bawaan, alih-alih menghitung seluruh katalog melalui ctx.modelRegistry.getAvailable().
ctx.signal
Sinyal pembatalan agen saat ini, atau undefined ketika tidak ada giliran agen yang aktif.
Gunakan ini untuk pekerjaan bersarang yang sadar akan pembatalan yang dimulai oleh penangan ekstensi, misalnya:
fetch(..., { signal: ctx.signal })- panggilan model yang menerima
signal - file atau pembantu proses yang menerima
AbortSignal
ctx.signal biasanya ditentukan selama event giliran aktif seperti tool_call, tool_result, message_update, dan turn_end.
Biasanya undefined dalam konteks idle atau non-turn seperti acara sesi, perintah ekstensi, dan pintasan diaktifkan saat pi idle.
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()
Kontrol aliran pembantu. ctx.isIdle() salah saat Pi sedang memproses proses agen, percobaan ulang otomatis, percobaan pemadatan otomatis, atau kelanjutan antrean.
ctx.shutdown()
Minta penutupan pi dengan baik.
- Mode interaktif: Ditunda hingga agen menganggur (setelah memproses semua pesan kemudi dan tindak lanjut yang diantri).
- RPC mode: Ditunda hingga status siaga berikutnya (setelah menyelesaikan respons perintah saat ini, saat menunggu perintah berikutnya).
- Mode cetak: Tanpa pengoperasian. Proses keluar secara otomatis ketika semua perintah diproses.
Memancarkan acara session_shutdown ke semua ekstensi sebelum keluar. Tersedia dalam semua konteks (event handler, alat, perintah, pintasan).
pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});ctx.getContextUsage()
Mengembalikan penggunaan konteks saat ini untuk model aktif. Menggunakan penggunaan asisten terakhir bila tersedia, lalu memperkirakan token untuk pesan tambahan.
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}ctx.kompak()
Memicu pemadatan tanpa menunggu selesai. Gunakan onComplete dan onError untuk tindakan tindak lanjut.
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()
Mengembalikan string perintah sistem Pi saat ini.
- Selama
before_agent_start, hal ini mencerminkan perubahan cepat sistem berantai yang dilakukan sejauh ini untuk belokan saat ini. - Ini tidak termasuk mutasi pesan
contextselanjutnya. - Ini tidak termasuk
before_provider_requestpenulisan ulang payload. - Jika ekstensi yang dimuat kemudian dijalankan setelah ekstensi Anda, ekstensi tersebut masih dapat mengubah ekstensi yang dikirimkan.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});EkstensiPerintahKonteks
Penangan perintah menerima ExtensionCommandContext, yang diperluas ExtensionContext dengan metode kontrol sesi. Ini hanya tersedia dalam perintah karena dapat menemui jalan buntu jika dipanggil dari event handler.
ctx.getSystemPromptOptions()
Mengembalikan input dasar Pi yang saat ini digunakan untuk membangun prompt sistem.
const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];Ini memiliki bentuk dan kemampuan berubah yang sama dengan before_agent_start event.systemPromptOptions: perintah khusus, alat aktif, cuplikan alat, pedoman perintah, teks perintah sistem yang ditambahkan, cwd, context files yang dimuat, dan keterampilan yang dimuat. Ini mungkin berisi konten file konteks penuh, jadi perlakukan itu sebagai data lokal ekstensi yang sensitif dan hindari memaparkannya melalui daftar perintah, log, atau metadata pelengkapan otomatis.
Ini melaporkan input prompt dasar saat ini. Ini tidak termasuk perubahan cepat sistem berantai before_agent_start per putaran, mutasi pesan peristiwa context di kemudian hari, atau penulisan ulang muatan before_provider_request.
ctx.waitForIdle()
Tunggu hingga agen menyelesaikan sepenuhnya, termasuk percobaan ulang otomatis, percobaan pemadatan otomatis, dan kelanjutan antrean:
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.sesi baru(pilihan?)
Buat sesi baru:
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
}Pilihan:
parentSession: file sesi induk untuk direkam di header sesi barusetup: mutasikanSessionManagersesi baru sebelumwithSessionberjalanwithSession: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakanpi/ perintahctxlama yang diambil; lihat Session replacement lifecycle and footguns.
ctx.fork(entryId, opsi?)
Cabang dari entri tertentu, membuat file sesi baru:
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
}Pilihan:
position:"before"(default) bercabang sebelum pesan pengguna yang dipilih, mengembalikan prompt itu ke editorposition:"at"menduplikasi jalur aktif melalui entri yang dipilih tanpa memulihkan teks editorwithSession: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakanpi/ perintahctxlama yang diambil; lihat Session replacement lifecycle and footguns.
ctx.navigateTree(targetId, opsi?)
Arahkan ke titik lain di 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",
});Pilihan:
summarize: Apakah akan membuat ringkasan cabang yang ditinggalkancustomInstructions: Instruksi khusus untuk peringkasreplaceInstructions: Jika benar,customInstructionsmenggantikan prompt default dan bukannya ditambahkanlabel: Label untuk dilampirkan pada entri ringkasan cabang (atau entri target jika tidak diringkas)
ctx.switchSession(sessionPath, opsi?)
Beralih ke file sesi lain:
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
}Pilihan:
withSession: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakanpi/ perintahctxlama yang diambil; lihat Session replacement lifecycle and footguns.
Untuk menemukan sesi yang tersedia, gunakan metode statis SessionManager.list() atau 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");
},
});
}
},
});Siklus hidup dan footgun penggantian sesi
withSession menerima ReplacedSessionContext baru, yang memperluas ExtensionCommandContext dengan pembantu asinkron sendMessage() dan sendUserMessage() yang terikat pada sesi penggantian.
Siklus hidup dan footgun:
withSessionberjalan hanya setelah sesi lama memancarkansession_shutdown, runtime lama telah dihapus, sesi pengganti telah di-rebound, dan instance ekstensi baru telah menerimasession_start.- Callback masih dijalankan di penutupan asli, bukan di dalam instance ekstensi baru. Itu berarti instance ekstensi lama Anda mungkin sudah menjalankan pembersihan penutupannya sebelum
withSessiondimulai. - Objek terikat sesi
pi/ perintah lamactxlama yang diambil akan menjadi basi setelah diganti dan akan dibuang jika digunakan. Gunakan hanyactxyang diteruskan kewithSessionuntuk pekerjaan terikat sesi. - Benda mentah yang diekstraksi sebelumnya tetap menjadi tanggung jawab Anda. Misalnya, jika Anda menangkap
const sm = ctx.sessionManagersebelum penggantian,smtetap menjadi objekSessionManageryang lama. Jangan menggunakannya kembali setelah penggantian. - Kode di
withSessionharus mengasumsikan status apa pun yang dibatalkan oleh pengendalisession_shutdownAnda sudah hilang. Hanya ambil data biasa yang bertahan saat dimatikan dengan bersih, seperti string, id, dan konfigurasi serial.
Pola aman:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});Pola tidak aman:
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()
Jalankan alur isi ulang yang sama seperti /reload.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});Perilaku penting:
await ctx.reload()memancarkansession_shutdownuntuk waktu proses ekstensi saat ini- Kemudian memuat ulang sumber daya dan mengeluarkan
session_startdenganreason: "reload"danresources_discoverdengan alasan"reload" - Pengendali perintah yang sedang berjalan masih berlanjut di bingkai panggilan lama
- Kode setelah
await ctx.reload()masih berjalan dari versi pra-muat ulang - Kode setelah
await ctx.reload()tidak boleh menganggap status ekstensi dalam memori yang lama masih valid - Setelah handler kembali, perintah/peristiwa/panggilan alat di masa mendatang menggunakan versi ekstensi baru
Untuk perilaku yang dapat diprediksi, perlakukan reload sebagai terminal untuk pengendali tersebut (await ctx.reload(); return;).
Alat dijalankan dengan ExtensionContext, sehingga tidak dapat memanggil ctx.reload() secara langsung. Gunakan perintah sebagai titik masuk muat ulang, lalu tampilkan alat yang mengantri perintah tersebut sebagai pesan pengguna tindak lanjut.
Contoh alat yang dapat dipanggil LLM untuk memicu pemuatan ulang:
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." }],
};
},
});
}EkstensiAPI Metode
pi.on(acara, pengendali)
Berlangganan acara. Lihat Events untuk jenis peristiwa dan nilai kembalian.
pi.registerTool(definisi)
Daftarkan alat khusus yang dapat dipanggil oleh LLM. Lihat Custom Tools untuk detail selengkapnya.
pi.registerTool() berfungsi selama pemuatan ekstensi dan setelah pengaktifan. Anda dapat memanggilnya di dalam session_start, pengendali perintah, atau pengendali kejadian lainnya. Alat baru segera disegarkan di sesi yang sama, sehingga muncul di pi.getAllTools() dan dapat dipanggil oleh LLM tanpa /reload.
Gunakan pi.setActiveTools() untuk mengaktifkan atau menonaktifkan alat (termasuk alat yang ditambahkan secara dinamis) saat runtime.
Gunakan promptSnippet untuk memasukkan alat khusus ke dalam entri satu baris di Available tools, dan promptGuidelines untuk menambahkan poin khusus alat ke bagian Guidelines default saat alat aktif.
Penting: promptGuidelines poin ditambahkan rata ke bagian Guidelines tanpa awalan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari "Gunakan alat ini ketika..." karena LLM tidak dapat membedakan alat mana yang dimaksud dengan "ini". Tulis "Gunakan my_tool ketika..." sebagai gantinya.
Lihat dynamic-tools.ts untuk contoh selengkapnya.
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(pesan, opsi?)
Masukkan pesan khusus ke dalam sesi. Pesan khusus berpartisipasi dalam konteks LLM. Untuk konten tahan lama TUI saja yang tidak boleh dikirim ke LLM, gunakan pi.appendEntry() dengan pi.registerEntryRenderer().
pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});Pilihan:
deliverAs- Modus pengiriman:"steer"(default) - Mengantrekan pesan saat streaming. Dikirim setelah giliran asisten saat ini selesai menjalankan panggilan alatnya, sebelum panggilan LLM berikutnya."followUp"- Menunggu agen selesai. Dikirim hanya ketika agen tidak lagi memiliki panggilan alat."nextTurn"- Mengantri untuk permintaan pengguna berikutnya. Tidak mengganggu atau memicu apa pun.
triggerTurn: true- Jika agen menganggur, segera picu respons LLM. Hanya berlaku untuk mode"steer"dan"followUp"(diabaikan untuk"nextTurn").
pi.sendUserMessage(konten, opsi?)
Kirim pesan pengguna ke agen. Berbeda dengan sendMessage() yang mengirimkan pesan khusus, ini mengirimkan pesan pengguna sebenarnya yang tampak seolah-olah diketik oleh pengguna. Selalu memicu belokan.
// 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" });Pilihan:
deliverAs- Diperlukan saat agen sedang streaming:"steer"- Mengantri pesan untuk dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya"followUp"- Menunggu agen menyelesaikan semua alat
Saat tidak streaming, pesan langsung terkirim dan memicu giliran baru. Saat streaming tanpa deliverAs, terjadi kesalahan.
Lihat send-user-message.ts untuk contoh lengkap.
pi.appendEntry(Tipe khusus, data?)
Pertahankan data ekstensi. Entri khusus TIDAK berpartisipasi dalam konteks LLM. Dalam mode interaktif, mereka juga dapat merender di dalam transkrip obrolan saat dipasangkan dengan 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(nama)
Tetapkan nama tampilan sesi (ditampilkan di pemilih sesi, bukan di pesan pertama).
pi.setSessionName("Refactor auth module");pi.getSessionName()
Dapatkan nama sesi saat ini, jika disetel.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, label)
Menetapkan atau menghapus label pada entri. Label adalah penanda yang ditentukan pengguna untuk bookmark dan navigasi (ditampilkan di pemilih /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);Label tetap ada dalam sesi dan bertahan saat dimulai ulang. Gunakan mereka untuk menandai titik-titik penting (belokan, pos pemeriksaan) di pohon percakapan.
pi.registerCommand(nama, opsi)
Daftarkan perintah.
Jika beberapa ekstensi mendaftarkan nama perintah yang sama, pi menyimpan semuanya dan menetapkan sufiks pemanggilan numerik dalam urutan pemuatan, misalnya /review:1 dan /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");
}
});Opsional: tambahkan argumen pelengkapan otomatis untuk /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()
Dapatkan slash commands tersedia untuk pemanggilan melalui prompt di sesi saat ini. Termasuk perintah ekstensi, prompt templates, dan perintah keterampilan.
Daftarnya cocok dengan urutan RPC get_commands: ekstensi terlebih dahulu, lalu templat, lalu keterampilan.
const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");Setiap entri memiliki bentuk ini:
{
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;
};
}Gunakan sourceInfo sebagai bidang asal kanonik. Jangan menyimpulkan kepemilikan dari nama perintah atau dari penguraian jalur ad hoc.
Perintah interaktif bawaan (seperti /model dan /settings) tidak disertakan di sini. Mereka ditangani hanya secara interaktif
mode dan tidak akan dijalankan jika dikirim melalui prompt.
pi.registerMessageRenderer(tipe khusus, penyaji)
Daftarkan penyaji TUI khusus untuk pesan khusus dengan customType Anda. Pesan khusus dibuat dengan pi.sendMessage() dan berpartisipasi dalam konteks LLM. Lihat Custom UI.
pi.registerMarkdownTransformator(transformator)
Daftarkan transformator untuk Markdown dalam teks pengguna normal, teks asisten, dan blok pemikiran. Trafo dijalankan dalam urutan beban ekstensi, dan setiap trafo menerima Markdown yang dikembalikan oleh trafo sebelumnya. Setelah rantai selesai, Pi merender konten yang diubah dengan penyaji bawaannya.
Transformator menerima string Markdown dan konteks dengan:
messageType—"user","assistant", atau"assistant-thinking"isStreaming—trueuntuk pembaruan sebagian asisten;falseuntuk pengguna, asisten yang diselesaikan, dan pesan yang dipulihkanavailableWidth— kolom terminal persis tersedia untuk konten Markdown yang diubah
Kembalikan Markdown yang telah diubah:
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});Jika trafo mati, Pi mempertahankan Markdown yang dihasilkan sejauh ini dan dilanjutkan dengan trafo berikutnya. Pengaitnya hanya untuk tampilan: pesan asli tetap tidak berubah dalam konteks sesi dan model. Ini berjalan untuk pesan pengguna baru, pembaruan streaming asisten, pesan sesi yang dipulihkan, dan perubahan lebar terminal, sehingga transformator harus tetap sinkron dan murah.
pi.registerEntryRenderer(tipe khusus, penyaji)
Daftarkan penyaji TUI khusus untuk entri khusus dengan customType Anda. Entri khusus dibuat dengan pi.appendEntry() dan tidak berpartisipasi dalam konteks 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(pintasan, opsi)
Daftarkan pintasan keyboard. Lihat keybindings.md untuk format pintasan dan pengikatan tombol bawaan.
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});pi.registerFlag(nama, opsi)
Daftarkan bendera CLI.
pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}pi.exec(perintah, argumen, opsi?)
Jalankan perintah shell.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(nama)
Kelola alat aktif. Ini berfungsi untuk alat bawaan dan alat yang terdaftar secara dinamis. pi.getActiveTools() mengembalikan nama alat aktif sebagai string[]; pi.getAllTools() mengembalikan metadata untuk semua alat yang dikonfigurasi.
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() mengembalikan name, description, parameters, promptGuidelines, dan sourceInfo.
Nilai sourceInfo.source yang umum:
builtinuntuk alat bawaansdkuntuk alat yang diteruskan melaluicreateAgentSession({ customTools })- metadata sumber ekstensi untuk alat yang didaftarkan oleh ekstensi
pi.setModel(model)
Tetapkan model saat ini. Mengembalikan false jika tidak ada API key yang tersedia untuk model. Lihat models.md untuk mengonfigurasi model khusus.
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(tingkat)
Dapatkan atau atur tingkat berpikir. Level disesuaikan dengan kemampuan model (model non-penalaran selalu menggunakan "mati"). Perubahan memancarkan thinking_level_select.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");pi.events
Bus acara bersama untuk komunikasi antar ekstensi:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(nama, konfigurasi)
Daftarkan atau ganti penyedia model secara dinamis. Berguna untuk proxy, titik akhir khusus, atau konfigurasi model seluruh tim.
Panggilan yang dilakukan selama fungsi pabrik ekstensi dimasukkan ke dalam antrean dan diterapkan setelah pelari melakukan inisialisasi. Panggilan yang dilakukan setelah itu — misalnya dari pengendali perintah yang mengikuti alur pengaturan pengguna — langsung berlaku tanpa memerlukan /reload.
Penyedia dinamis dapat menerapkan refreshModels. Pi memanggilnya selama penyegaran model, menerbitkan daftar yang dikembalikan secara sinkron melalui penyedia, dan meneruskan konteks kredensial/katalog tersimpan/jaringan/sinyal kanonik. Ekstensi memutuskan apakah akan mempertahankan metadata katalog melalui pemeriksaan generasi context.publish({ persist: entry }); server langsung seperti llama.cpp dapat mengembalikan model tanpa menyimpannya.
context.signal selalu merupakan sinyal konkret dan callback penyedia harus meneruskannya ke pemblokiran I/O. Panggilan publik ModelRuntime.refresh() dan ModelRegistry.refresh() menerima sinyal opsional dan tidak dibatasi jika dihilangkan; ekstensi dan aplikasi memilih tenggat waktu mereka sendiri. Pembatalan menghentikan penelpon menunggu meskipun penyedia mengabaikan sinyalnya, namun kerja sama tetap diperlukan untuk menghentikan pekerjaan yang mendasarinya.
Extensions yang memerlukan autentikasi, pemfilteran, penyegaran, atau perilaku streaming penyedia asli dapat mendaftarkan Provider lengkap dari @earendil-works/pi-ai. Penyedia menjadi basis komposisi dan penggantian models.json masih berlaku di atasnya.
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;
}
}
});Bentuk objek menerima pi-ai lengkap Provider, termasuk perilaku asli auth, getModels, refreshModels, filterModels, stream, dan streamSimple.
Opsi konfigurasi lama:
name- Nama tampilan untuk penyedia di UI seperti/login.baseUrl- API URL titik akhir. Diperlukan saat mendefinisikan model.apiKey- API key literal, interpolasi lingkungan ($ENV_VARatau${ENV_VAR}), atau awalan!command. Diperlukan saat menentukan model (kecualioauthdisediakan).$lolos dari ``apiKey- API key literal, interpolasi lingkungan ($ENV_VARatau${ENV_VAR}), atau awalan!command. Diperlukan saat menentukan model (kecualioauthdisediakan).$lolos dari, dan$!lolos dari!` literal tanpa memicu eksekusi perintah.api- API ketik:"anthropic-messages","openai-completions","openai-responses", dll.headers- Header khusus untuk disertakan dalam permintaan.authHeader- Jika benar, tambahkan headerAuthorization: Bearersecara otomatis.models- Kumpulan definisi model. Jika tersedia, gantikan semua model yang ada untuk penyedia ini. Definisi model dapat mengaturbaseUrluntuk mengganti titik akhir penyedia untuk model tersebut.refreshModels- Panggilan balik penemuan dinamis asinkron. Model yang dikembalikan menggantikan model yang disediakan ekstensi.context.storedberisi snapshot penyedia yang ada; gunakan generasi-diperiksacontext.publish({ persist: entry })hanya ketika data katalog yang diperbarui harus tetap ada. Gunakanpersist: nulluntuk menghapus snapshot itu.oauth- OAuth konfigurasi penyedia untuk dukungan/login. Jika disediakan, penyedia muncul di menu login.streamSimple- Implementasi streaming khusus untuk API non-standar.
Lihat custom-provider.md untuk topik lanjutan: streaming khusus APIs, detail OAuth, referensi definisi model.
pi.unregisterProvider(nama)
Hapus penyedia yang terdaftar sebelumnya dan modelnya. Model bawaan yang diganti oleh penyedia akan dipulihkan. Tidak berpengaruh jika penyedia tidak terdaftar.
Seperti registerProvider, ini berlaku segera ketika dipanggil setelah fase beban awal, jadi /reload tidak diperlukan.
pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});Manajemen Negara
Extensions dengan negara harus menyimpannya dalam hasil alat details untuk dukungan percabangan yang tepat:
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
};
},
});
}Alat Kustom
Daftarkan alat yang dapat dihubungi LLM melalui pi.registerTool(). Alat muncul di prompt sistem dan dapat memiliki rendering khusus.
Gunakan promptSnippet untuk entri satu baris pendek di bagian Available tools pada prompt sistem default. Jika dihilangkan, alat khusus tidak dimasukkan dalam bagian itu.
Gunakan promptGuidelines untuk menambahkan poin khusus alat ke bagian prompt sistem default Guidelines. Poin-poin ini hanya disertakan saat alat aktif (misalnya, setelah pi.setActiveTools([...])).
Penting: promptGuidelines poin ditambahkan rata ke bagian Guidelines tanpa awalan atau pengelompokan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari "Gunakan alat ini ketika..." karena LLM tidak dapat membedakan alat mana yang dimaksud dengan "ini". Tulis "Gunakan my_tool ketika..." sebagai gantinya.
Catatan: Beberapa model bodoh dan menyertakan awalan @ dalam argumen jalur alat. Alat bawaan menghapus @ terdepan sebelum menyelesaikan jalur. Jika alat khusus Anda menerima jalur, normalkan juga @ di depannya.
Jika alat khusus Anda memutasi file, gunakan withFileMutationQueue() sehingga alat tersebut berpartisipasi dalam antrean per file yang sama dengan edit dan write bawaan. Hal ini penting karena pemanggilan alat dijalankan secara paralel secara default. Tanpa antrian, dua alat dapat membaca konten file lama yang sama, menghitung pembaruan yang berbeda, dan kemudian penulisan mana pun yang terakhir akan menimpa yang lain.
Contoh kasus kegagalan: alat khusus Anda mengedit foo.ts sementara edit bawaan juga mengubah foo.ts pada giliran asisten yang sama. Jika alat Anda tidak berpartisipasi dalam antrean, keduanya dapat membaca foo.ts asli, menerapkan perubahan terpisah, dan salah satu perubahan tersebut akan hilang.
Teruskan jalur file target sebenarnya ke withFileMutationQueue(), bukan argumen pengguna mentah. Selesaikan terlebih dahulu ke jalur absolut, relatif terhadap ctx.cwd atau direktori kerja alat Anda. Untuk file yang sudah ada, helper melakukan kanonikalisasi melalui realpath(), jadi alias symlink untuk file yang sama berbagi satu antrian. Untuk file baru, file tersebut kembali ke jalur absolut yang diselesaikan karena belum ada apa pun untuk realpath().
Antrian seluruh jendela mutasi pada jalur target itu. Itu termasuk logika baca-modifikasi-tulis, bukan hanya penulisan akhir.
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: {},
};
});
}Definisi Alat
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) { ... },
});Penghitungan penggunaan: Jika alat melakukan panggilan LLM bertingkat, kembalikan gabungan Usage sebagai usage. Pi menyimpannya pada hasil alat dan memasukkannya ke dalam footer, /session, dan RPC total sesi. tool_result penangan dapat memeriksa atau mengganti nilai ini.
Kesalahan sinyal: Untuk menandai eksekusi alat sebagai gagal (menetapkan isError: true pada hasil dan melaporkannya ke LLM), memunculkan kesalahan dari execute. Mengembalikan nilai tidak pernah menyetel tanda kesalahan apa pun properti yang Anda sertakan dalam objek pengembalian.
Penghentian awal: Kembalikan terminate: true dari execute() untuk memberi petunjuk bahwa panggilan LLM tindak lanjut otomatis harus dilewati setelah kumpulan alat saat ini. Ini hanya berlaku ketika setiap hasil alat yang diselesaikan dalam batch tersebut dihentikan. Lihat examples/extensions/structured-output.ts untuk contoh minimal saat agen mengakhiri panggilan alat keluaran terstruktur akhir.
// 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: {} };
}Penting: Gunakan StringEnum dari @earendil-works/pi-ai untuk enum string. Type.Union/Type.Literal tidak berfungsi dengan API Google.
Persiapan argumen: prepareArguments(args) bersifat opsional. Jika ditentukan, ini berjalan sebelum validasi skema dan sebelum execute(). Gunakan ini untuk meniru bentuk input lama yang diterima ketika pi melanjutkan sesi lama yang argumen pemanggilan alatnya tidak lagi cocok dengan skema saat ini. Kembalikan objek yang ingin Anda validasi terhadap parameters. Jaga agar skema publik tetap ketat. Jangan menambahkan bidang kompatibilitas yang tidak digunakan lagi ke parameters hanya agar sesi lama yang dilanjutkan tetap berfungsi.
Contoh: sesi lama mungkin berisi panggilan alat edit dengan oldText dan newText tingkat atas, sedangkan skema saat ini hanya menerima 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: {},
};
},
});Mengganti Alat Bawaan
Extensions dapat mengganti alat bawaan (read, bash, edit, write, grep, find, ls) dengan mendaftarkan alat dengan nama yang sama. Mode interaktif menampilkan peringatan ketika hal ini terjadi.
# Extension's read tool replaces built-in read
pi -e ./tool-override.tsAlternatifnya, gunakan --no-builtin-tools untuk memulai tanpa alat bawaan apa pun sambil tetap mengaktifkan alat ekstensi:
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.tsLihat examples/extensions/tool-override.ts untuk contoh lengkap yang menggantikan read dengan logging dan kontrol akses.
Rendering: Warisan perender bawaan diselesaikan per slot. Penimpaan eksekusi dan pengesampingan rendering bersifat independen. Jika penggantian Anda menghilangkan renderCall, renderCall bawaan akan digunakan. Jika penggantian Anda menghilangkan renderResult, renderResult bawaan akan digunakan. Jika penggantian Anda menghilangkan keduanya, penyaji bawaan akan digunakan secara otomatis (penyorotan sintaksis, perbedaan, dll.). Hal ini memungkinkan Anda menggabungkan alat bawaan untuk logging atau kontrol akses tanpa mengimplementasikan ulang UI.
Metadata cepat: promptSnippet dan promptGuidelines tidak diwarisi dari alat bawaan. Jika penggantian Anda harus menyimpan instruksi cepat tersebut, tentukan instruksi tersebut pada penggantian secara eksplisit.
Penerapan Anda harus sesuai dengan bentuk hasil yang tepat, termasuk jenis details. Logika UI dan sesi bergantung pada bentuk ini untuk rendering dan pelacakan status.
Implementasi alat bawaan:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Eksekusi Jarak Jauh
Alat bawaan mendukung operasi yang dapat dicolokkan untuk mendelegasikan ke sistem jarak jauh (SSH, container, dll.):
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);
},
});Antarmuka operasi: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
Untuk user_bash, ekstensi dapat menggunakan kembali backend shell lokal pi melalui createLocalBashOperations() alih-alih mengimplementasikan ulang pemijahan proses lokal, resolusi shell, dan penghentian pohon proses.
Alat bash juga mendukung spawn hook untuk menyesuaikan perintah, cwd, atau env sebelum eksekusi:
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() memaparkan sesi saat ini ke perintah melalui PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL, dan PI_REASONING_LEVEL. Injeksi terjadi sebelum spawnHook, jadi hook menerima nilai ini di env dan mempertahankannya saat menyebarkan lingkungan yang ada seperti di atas. Atur exposeSessionEnvironment: false untuk menonaktifkannya:
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});Lihat Bash tool session environment untuk semantik variabel. Lihat examples/extensions/ssh.ts untuk contoh SSH lengkap dengan tanda --ssh.
Pemotongan Keluaran
Alat HARUS memotong keluarannya untuk menghindari konteks LLM yang berlebihan. Output yang besar dapat menyebabkan:
- Kesalahan luapan konteks (prompt terlalu panjang)
- Kegagalan pemadatan
- Performa model menurun
Batas bawaannya adalah 50KB (~10 ribu token) dan 2000 baris, mana saja yang tercapai terlebih dahulu. Gunakan utilitas pemotongan yang diekspor:
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 }] };
}Poin-poin penting:
- Gunakan
truncateHeaduntuk konten yang bagian awalnya penting (hasil pencarian, pembacaan file) - Gunakan
truncateTailuntuk konten yang ujungnya penting (log, keluaran perintah) - Selalu beri tahu LLM ketika keluaran terpotong dan di mana menemukan versi lengkapnya
- Dokumentasikan batas pemotongan dalam deskripsi alat Anda
Lihat examples/extensions/truncated-tool.ts untuk contoh lengkap membungkus rg (ripgrep) dengan pemotongan yang tepat.
Berbagai Alat
Satu ekstensi dapat mendaftarkan beberapa alat dengan status bersama:
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();
});
}Rendering Kustom
Alat dapat menyediakan renderCall dan renderResult untuk tampilan TUI khusus. Lihat tui.md untuk komponen lengkap API dan tool-execution.ts untuk mengetahui bagaimana baris alat disusun.
Secara default, keluaran alat dibungkus dengan Box yang menangani padding dan latar belakang. renderCall atau renderResult yang ditentukan harus menghasilkan Component. Jika penyaji slot tidak ditentukan, tool-execution.ts menggunakan rendering cadangan untuk slot tersebut.
Setel renderShell: "self" kapan alat harus merender shellnya sendiri alih-alih menggunakan Box default. Hal ini berguna untuk alat yang memerlukan kontrol penuh atas perilaku pembingkaian atau latar belakang, misalnya pratinjau besar yang harus tetap stabil secara visual setelah alat tersebut dipasang.
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 dan renderResult masing-masing menerima objek context dengan:
args- argumen pemanggilan alat saat inistate- status baris-lokal bersama direnderCalldanrenderResultlastComponent- komponen yang dikembalikan sebelumnya untuk slot tersebut, jika adainvalidate()- meminta rendering baris alat initoolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
Gunakan context.state untuk status bersama lintas slot. Simpan cache slot-lokal pada instance komponen yang dikembalikan ketika Anda ingin menggunakan kembali dan mengubah komponen yang sama di seluruh render.
panggilan render
Merender panggilan alat atau header:
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;
}hasil render
Merender hasil atau keluaran alat:
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);
}Jika slot sengaja tidak memiliki konten yang terlihat, kembalikan Component kosong seperti Container kosong.
Petunjuk Pengikatan Kunci
Gunakan keyHint() untuk menampilkan petunjuk pengikatan kunci yang mengikuti konfigurasi pengikatan kunci aktif:
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);
}Fungsi yang tersedia:
keyHint(keybinding, description)- Memformat id pengikat kunci yang dikonfigurasi seperti"app.tools.expand"atau"tui.select.confirm"keyText(keybinding)- Mengembalikan teks kunci mentah yang dikonfigurasi untuk id pengikat kuncirawKeyHint(key, description)- Memformat string kunci mentah
Gunakan id pengikat kunci dengan spasi nama:
- Id agen pengkodean menggunakan namespace
app.*, misalnyaapp.tools.expand,app.editor.external,app.session.rename - ID TUI yang dibagikan menggunakan namespace
tui.*, misalnyatui.select.confirm,tui.select.cancel,tui.input.tab
Untuk daftar lengkap id dan default pengikat kunci, lihat keybindings.md. keybindings.json menggunakan id dengan namespace yang sama.
Editor khusus dan komponen ctx.ui.custom() menerima keybindings: KeybindingsManager sebagai argumen yang dimasukkan. Mereka harus menggunakan manajer yang disuntikkan itu secara langsung daripada menelepon getKeybindings() atau setKeybindings().
Praktik Terbaik
- Gunakan
Textdengan bantalan(0, 0). Kotak default menangani padding. - Gunakan
\nuntuk konten multi-baris. - Tangani
isPartialuntuk kemajuan streaming. - Dukungan
expandeduntuk detail sesuai permintaan. - Pertahankan tampilan default tetap ringkas.
- Baca
context.argsdirenderResultalih-alih menyalin argumen kecontext.state. - Gunakan
context.statehanya untuk data yang harus dibagikan ke seluruh slot panggilan dan hasil. - Gunakan kembali
context.lastComponentketika instance komponen yang sama dapat diperbarui di tempatnya. - Gunakan
renderShell: "self"hanya ketika shell kotak default menghalangi. Dalam mode self-shell, alat ini bertanggung jawab atas framing, padding, dan latar belakangnya sendiri.
Penggantian
Jika penyaji slot tidak ditentukan atau muncul:
renderCall: Menampilkan nama alatrenderResult: Menampilkan teks mentah daricontent
Pemuatan Alat Dinamis
Extensions dapat mendaftarkan banyak alat sambil tetap mengaktifkan set awal kecil. Sebuah alat kemudian dapat menambahkan lebih banyak alat dengan pi.setActiveTools() selama eksekusi. Pi mendeteksi perubahan aditif murni, mencatat nama alat yang baru tersedia pada hasil alat tersebut, dan menerapkan set aktif yang diperbarui sebelum permintaan model berikutnya.
Ini berfungsi pada setiap model. Models dengan dukungan pemuatan tertunda asli mempertahankan awalan prompt stabil dan memuat definisi baru pada posisi hasil pahat. Model lain menggunakan fallback yang dijelaskan di bawah.
Siklus hidupnya adalah:
- Daftarkan setiap alat dengan
pi.registerTool()sehingga muncul dipi.getAllTools(). - Biarkan alat pemuat, seperti
search_tools, tetap aktif dan biarkan alat yang dapat dicari tidak aktif. - Selama eksekusi loader, panggil
pi.setActiveTools([...currentTools,...matchingTools]). Perubahannya harus bersifat tambahan: jangan menghapus alat yang sedang aktif dalam panggilan yang sama. - Pi mencatat alat mana yang ditambahkan pada hasil alat pemuat.
- Sebelum respons model berikutnya, Pi memaparkan definisi tambahan menggunakan pemuatan asli yang ditangguhkan jika didukung, atau daftar alat aktif normal sebaliknya.
Anda tidak perlu mengembalikan referensi alat khusus penyedia atau menandai pemuat sebagai alat pencarian khusus. Perubahan alat aktif adalah sinyalnya. Nama yang diteruskan ke pi.setActiveTools() harus sudah terdaftar; nama yang tidak diketahui diabaikan.
Models dengan pemuatan tertunda asli
- Antropik
- Models: Soneta, Opus, Fable versi 4.5 atau lebih baru (tanpa Haiku)
- Representasi asli: Definisi yang ditangguhkan menggunakan
defer_loading; titik muat menggunakan kontentool_reference.
- BukaAI
- Models:
gpt-5.4dan keluarga baru - Representasi asli: Pi menambahkan item klien
tool_search_calldantool_search_outputyang telah selesai pada titik pemuatan.
- Models:
Untuk model atau proksi khusus yang terverifikasi, penanganan asli dapat diaktifkan dengan compat.supportsToolReferences: true untuk anthropic-messages, atau compat.supportsToolSearch: true untuk openai-responses dan openai-codex-responses. Biarkan ini dinonaktifkan kecuali titik akhir dan model menerima protokol asli yang sesuai.
Perilaku mundur
Untuk semua model dan penyedia lainnya, aktivasi dinamis masih berfungsi: Pi mengirimkan daftar lengkap alat aktif saat ini secara normal pada permintaan berikutnya. Model dapat memanggil alat yang baru diaktifkan, namun menambahkan definisinya dapat membuat awalan prompt cache penyedia menjadi tidak valid.
Pi juga menggunakan fallback aman ini ketika set aktif tidak murni bersifat aditif, seperti mengganti satu kelompok alat dengan kelompok alat lainnya. Oleh karena itu, penghapusan alat dapat dilakukan, tetapi tidak menggunakan pemuatan yang ditangguhkan.
Untuk perilaku cache terbaik, biarkan alat pemuat tetap aktif sepanjang sesi dan tambahkan alat alih-alih mengganti set yang aktif. Perhatikan juga bahwa mengaktifkan alat dengan promptSnippet atau promptGuidelines akan membangun kembali prompt sistem; perubahan yang dilakukan segera oleh sistem dapat membatalkan awalan meskipun penyedia mendukung skema yang ditangguhkan. Alat yang dimuat dengan lambat biasanya harus mengandalkan alatnya description dan menghilangkan metadata prompt yang hanya aktif.
Contoh alat pencarian
Ekstensi berikut mendaftarkan dua alat yang dapat dicari, menghapusnya dari set aktif awal, dan hanya menyimpan search_tools sebagai pemuatnya. Contohnya menggunakan pencocokan kata kunci sederhana, namun penerapan penelusuran dapat menggunakan BM25, penyematan, katalog jarak jauh, atau perutean khusus proyek.
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"])]);
});
}Saat search_tools menambahkan kecocokan, model menerima definisi tersebut segera setelah permintaan. Pada model berkemampuan asli, definisi tersebut ditetapkan setelah hasil pencarian tanpa mengubah awalan skema alat awal. Pada model lain, alat ini muncul dalam daftar alat normal berdasarkan permintaan berikut yang sama.
UI khusus
Extensions dapat berinteraksi dengan pengguna melalui metode ctx.ui dan menyesuaikan cara pesan/alat ditampilkan.
Untuk komponen khusus, lihat tui.md yang memiliki pola salin-tempel untuk:
- Dialog pemilihan (SelectList)
- Operasi asinkron dengan pembatalan (BorderedLoader)
- Pengalih pengaturan (Daftar Pengaturan)
- Indikator status (setStatus)
- Pesan, visibilitas, dan indikator yang berfungsi selama streaming (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Widget di atas/di bawah editor (setWidget)
- Penyedia pelengkapan otomatis yang berlapis di atas garis miring/penyelesaian jalur bawaan (addAutocompleteProvider)
- Footer khusus (setFooter)
Dialog
// 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"Dialog Jangka Waktu dengan Hitung Mundur
Dialog mendukung opsi timeout yang ditutup secara otomatis dengan tampilan hitung mundur langsung:
// 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
}Nilai pengembalian saat batas waktu habis:
select()mengembalikanundefinedconfirm()mengembalikanfalseinput()mengembalikanundefined
Pemberhentian Manual dengan AbortSignal
Untuk kontrol lebih lanjut (misalnya, untuk membedakan batas waktu dari pembatalan pengguna), gunakan 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")
}Lihat examples/extensions/timed-confirm.ts untuk contoh lengkap.
Widget, Status, dan Footer
// 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 themeBingkai indikator kerja khusus ditampilkan secara verbatim. Jika Anda menginginkan warna, tambahkan sendiri warna tersebut ke string bingkai, misalnya dengan ctx.ui.theme.fg(...).
Pelengkapan otomatis Providers
Gunakan ctx.ui.addAutocompleteProvider() untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. Setel triggerCharacters untuk pemicu alami khusus seperti Gunakan ctx.ui.addAutocompleteProvider()untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. SeteltriggerCharacters` untuk pemicu alami khusus seperti.
Pola khas:
- periksa teks sebelum kursor
- kembalikan saran Anda sendiri ketika sintaks khusus ekstensi Anda cocok
- jika tidak, delegasikan ke
current.getSuggestions(...) - delegasikan
applyCompletion(...)kecuali Anda memerlukan perilaku penyisipan khusus
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;
},
}));
});Lihat github-issue-autocomplete.ts untuk contoh lengkap yang memuat masalah GitHub terbuka terbaru dengan gh issue list dan memfilternya secara lokal untuk penyelesaian #... yang cepat. Ini memerlukan GitHub CLI (gh) dan checkout repositori GitHub.
Komponen Khusus
Untuk UI yang kompleks, gunakan ctx.ui.custom(). Ini untuk sementara menggantikan editor dengan komponen Anda hingga done() dipanggil:
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
}Panggilan balik menerima:
tui- TUI contoh (untuk dimensi layar, manajemen fokus)theme- Tema penataan gaya saat inikeybindings- Manajer pengikat tombol aplikasi (untuk memeriksa pintasan)done(value)- Panggilan untuk menutup komponen dan mengembalikan nilai
Lihat tui.md untuk komponen lengkap API.
Mode Hamparan (Eksperimental)
Teruskan { overlay: true } untuk merender komponen sebagai modal mengambang di atas konten yang sudah ada, tanpa mengosongkan layar:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);Untuk pemosisian lanjutan (jangkar, margin, persentase, visibilitas responsif), teruskan overlayOptions. Gunakan onHandle untuk mengontrol fokus atau visibilitas secara terprogram:
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
}
}
);Hamparan terlihat terfokus dapat memperoleh kembali masukan setelah UI khusus non-hamparan sementara ditutup. Jika Anda sengaja ingin komponen lain tetap memasukkan input sementara overlay tetap terlihat, panggil handle.unfocus({ target }). Melewati { target: null } akan melepaskan overlay tanpa memfokuskan komponen lain.
Lihat tui.md untuk OverlayOptions lengkap dan OverlayHandle API dan overlay-qa-tests.ts sebagai contoh.
Editor Kustom
Ganti editor masukan utama dengan implementasi khusus (mode vim, mode emacs, dll.):
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)
);
});
}Poin-poin penting:
- Perpanjang
CustomEditor(bukan basisEditor) untuk mendapatkan ikatan kunci aplikasi (escape untuk membatalkan, ctrl+d, peralihan model) - Hubungi
super.handleInput(data)untuk kunci yang tidak Anda tangani - Pabrik menerima
tui,theme, dankeybindingsdari aplikasi - Gunakan
ctx.ui.getEditorComponent()sebelumsetEditorComponent()untuk menggabungkan editor khusus yang telah dikonfigurasi sebelumnya - Lewati
undefineduntuk mengembalikan default:ctx.ui.setEditorComponent(undefined)
Untuk menulis dengan ekstensi lain yang telah menggantikan editor, ambil pabrik sebelumnya sebelum mengatur milik Anda:
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);Lihat tui.md Pola 7 untuk contoh lengkap dengan indikator mode.
Rendering Pesan dan Entri
Daftarkan penyaji khusus untuk pesan dengan customType Anda. Gunakan penyaji pesan untuk konten yang harus berpartisipasi dalam konteks 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);
});Pesan dikirim melalui pi.sendMessage():
pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});Untuk konten khusus TUI yang tidak boleh dikirim ke LLM, render entri khusus sebagai gantinya:
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });Warna Tema
Semua fungsi render menerima objek theme. Lihat themes.md untuk membuat tema khusus dan palet warna lengkap.
// 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)Untuk penyorotan sintaksis pada perender alat khusus:
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);Penanganan Kesalahan
- Kesalahan ekstensi dicatat, agen melanjutkan
tool_callkesalahan memblokir alat (aman dari kegagalan)- Kesalahan alat
executeharus ditandai dengan melempar; kesalahan yang terjadi ditangkap, dilaporkan ke LLM denganisError: true, dan eksekusi dilanjutkan
Modus Perilaku
| Mode | ctx.mode |
ctx.hasUI |
Catatan |
|---|---|---|---|
| Interaktif | "tui" |
true |
Penuh TUI dengan rendering terminal |
RPC (--mode rpc) |
"rpc" |
true |
Dialog dan pemberitahuan melalui protokol JSON; custom() mengembalikan undefined. Lihat rpc.md |
JSON (--mode json) |
"json" |
false |
Aliran acara ke stdout; Metode UI tidak boleh dilakukan |
Cetak (-p) |
"print" |
false |
Extensions dijalankan tetapi tidak dapat meminta |
Gunakan ctx.mode === "tui" sebelum TUI fitur khusus (custom(), pabrik komponen, input terminal). Gunakan ctx.hasUI sebelum dialog dan metode pemberitahuan yang berfungsi dalam mode TUI dan RPC.
Contoh Referensi
Semua contoh di examples/extensions/.
| Contoh | Keterangan | Kunci APIs |
|---|---|---|
| Peralatan | ||
hello.ts |
Registrasi alat minimal | registerTool |
question.ts |
Alat dengan interaksi pengguna | registerTool, ui.select |
questionnaire.ts |
Alat penyihir multi-langkah | registerTool, ui.custom |
todo.ts |
Alat stateful dengan ketekunan | registerTool, appendEntry, renderResult, acara sesi |
dynamic-tools.ts |
Daftarkan alat setelah startup dan selama perintah | registerTool, session_start, registerCommand |
structured-output.ts |
Alat keluaran terstruktur akhir dengan terminate: true |
registerTool, menghentikan hasil alat |
truncated-tool.ts |
Contoh pemotongan keluaran | registerTool, truncateHead |
tool-override.ts |
Ganti alat baca bawaan | registerTool (nama yang sama dengan bawaan) |
| Perintah | ||
pirate.ts |
Ubah perintah sistem per putaran | registerCommand, before_agent_start |
summarize.ts |
Perintah ringkasan percakapan | registerCommand, ui.custom |
handoff.ts |
Penyerahan model lintas penyedia | registerCommand, ui.editor, ui.custom |
qna.ts |
Tanya Jawab dengan UI khusus | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
Menyuntikkan pesan pengguna | registerCommand, sendUserMessage |
reload-runtime.ts |
Muat ulang perintah dan handoff alat LLM | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
Perintah mematikan dengan baik | registerCommand, shutdown() |
| Acara & Gerbang | ||
permission-gate.ts |
Blokir perintah berbahaya | on("tool_call"), ui.confirm |
project-trust.ts |
Memutuskan atau menunda kepercayaan proyek dari pengguna/ekstensi global atau CLI | on("project_trust"), percayai UI, diperlukan hasil kepercayaan |
protected-paths.ts |
Blokir penulisan ke jalur tertentu | on("tool_call") |
confirm-destructive.ts |
Konfirmasikan perubahan sesi | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
Peringatkan tentang repo git yang kotor | on("session_before_*"), exec |
input-transform.ts |
Ubah masukan pengguna | on("input") |
input-transform-streaming.ts |
Transformasi masukan yang sadar streaming | on("input"), streamingBehavior |
model-status.ts |
React untuk memodelkan perubahan | on("model_select"), setStatus |
provider-payload.ts |
Periksa muatan dan header respons penyedia | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
Menampilkan info cepat sistem | on("agent_start"), getSystemPrompt |
claude-rules.ts |
Muat aturan dari file | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
Tambahkan panduan alat peka konteks menggunakan systemPromptOptions |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
Pengamat file memicu pesan | sendMessage |
| Pemadatan & Sesi | ||
custom-compaction.ts |
Ringkasan pemadatan khusus | on("session_before_compact") |
trigger-compact.ts |
Memicu pemadatan secara manual | compact() |
git-checkpoint.ts |
Git simpanan di tikungan | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
Ambil, gabungkan, dan selesaikan konflik | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
Berkomitmen untuk mematikan | on("session_shutdown"), exec |
| Komponen UI | ||
status-line.ts |
Indikator status catatan kaki | setStatus, acara sesi |
working-indicator.ts |
Sesuaikan indikator kerja streaming | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
Tambahkan #1234 penyelesaian masalah di atas pelengkapan otomatis bawaan dengan memuat terlebih dahulu masalah terbuka terkini dari gh issue list |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
Ganti footer seluruhnya | registerCommand, setFooter |
custom-header.ts |
Ganti header permulaan | on("session_start"), setHeader |
modal-editor.ts |
Editor modal bergaya Vim | setEditorComponent, CustomEditor |
rainbow-editor.ts |
Gaya editor khusus | setEditorComponent |
widget-placement.ts |
Widget di atas/di bawah editor | setWidget |
overlay-test.ts |
Komponen hamparan | ui.custom dengan opsi hamparan |
overlay-qa-tests.ts |
Tes overlay yang komprehensif | ui.custom, semua opsi hamparan |
notify.ts |
Pemberitahuan sederhana | ui.notify |
timed-confirm.ts |
Dialog dengan batas waktu | ui.confirm dengan batas waktu/sinyal |
mac-system-theme.ts |
Beralih tema secara otomatis | setTheme, exec |
| Kompleks Extensions | ||
plan-mode/ |
Implementasi mode rencana penuh | Semua jenis acara, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
Preset yang dapat disimpan (model, alat, pemikiran) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
Mengaktifkan/menonaktifkan UI alat | registerCommand, setActiveTools, SettingsList, acara sesi |
| Jarak Jauh & Kotak Pasir | ||
ssh.ts |
SSH eksekusi jarak jauh | registerFlag, on("user_bash"), on("before_agent_start"), pengoperasian alat |
interactive-shell.ts |
Sesi shell yang persisten | on("user_bash") |
sandbox/ |
Eksekusi alat dalam kotak pasir | Operasi alat |
gondolin/ |
Rutekan alat bawaan dan perintah ! ke dalam Gondolin mikro-VM |
Pengoperasian alat, penggantian alat bawaan, on("user_bash") |
subagent/ |
Memunculkan sub-agen | registerTool, exec |
| Pertandingan | ||
snake.ts |
Permainan ular | registerCommand, ui.custom, penanganan keyboard |
space-invaders.ts |
Permainan Penjajah Luar Angkasa | registerCommand, ui.custom |
doom-overlay/ |
Malapetaka dalam hamparan | ui.custom dengan hamparan |
| Providers | ||
custom-provider-anthropic/ |
Proksi Antropik Kustom | registerProvider |
custom-provider-gitlab-duo/ |
GitIntegrasi Lab Duo | registerProvider dengan OAuth |
| Pesan & Komunikasi | ||
message-renderer.ts |
Render pesan khusus | registerMessageRenderer, sendMessage |
entry-renderer.ts |
TUI-hanya rendering entri khusus | registerEntryRenderer, appendEntry |
event-bus.ts |
Acara antar-ekstensi | pi.events |
| Metadata Sesi | ||
session-name.ts |
Sesi nama untuk pemilih | setSessionName, getSessionName |
bookmark.ts |
Tandai entri untuk /pohon | setLabel |
| Lain-lain | ||
inline-bash.ts |
Sebaris bash dalam panggilan alat | on("tool_call") |
bash-spawn-hook.ts |
Sesuaikan perintah bash, cwd, dan env sebelum dieksekusi | createBashTool, spawnHook |
with-deps/ |
Ekstensi dengan dependensi npm | Struktur paket dengan package.json |