Extensions
pi có thể tạo tiện ích mở rộng. Yêu cầu nó xây dựng một cái cho trường hợp sử dụng của bạn.
Extensions là TypeScript mô-đun mở rộng hành vi của pi. Họ có thể đăng ký các sự kiện trong vòng đời, đăng ký các công cụ tùy chỉnh mà LLM có thể gọi, thêm lệnh, v.v.
Vị trí dành cho /tải lại: Đặt tiện ích mở rộng ở
~/.pi/agent/extensions/(toàn cầu) hoặc.pi/extensions/(dự án cục bộ) để tự động khám phá. Chỉ sử dụngpi -e./path.tscho các bài kiểm tra nhanh. Extensions ở các vị trí được tự động phát hiện có thể được tải lại nóng bằng/reload.
Các khả năng chính:
- Công cụ tùy chỉnh - Đăng ký công cụ mà LLM có thể gọi qua
pi.registerTool() - Chặn sự kiện - Chặn hoặc sửa đổi lệnh gọi công cụ, chèn ngữ cảnh, tùy chỉnh nén
- Tương tác người dùng - Nhắc người dùng qua
ctx.ui(chọn, xác nhận, nhập, thông báo) - Thành phần giao diện người dùng tùy chỉnh - Thành phần TUI đầy đủ với đầu vào bàn phím qua
ctx.ui.custom()cho các tương tác phức tạp - Lệnh tùy chỉnh - Đăng ký các lệnh như
/mycommandquapi.registerCommand() - Tính bền bỉ của phiên - Lưu trữ trạng thái vẫn tồn tại khi khởi động lại thông qua
pi.appendEntry() - Hiển thị tùy chỉnh - Kiểm soát cách các lệnh gọi/kết quả và thông báo của công cụ xuất hiện trong TUI
Các trường hợp sử dụng ví dụ:
- Cổng cấp phép (xác nhận trước
rm -rf,sudo, v.v.) - Git điểm kiểm tra (lưu trữ ở mỗi lượt, khôi phục trên nhánh)
- Bảo vệ đường dẫn (chặn ghi vào
.env,node_modules/) - Thu gọn tùy chỉnh (tóm tắt cuộc trò chuyện theo cách của bạn)
- Tóm tắt cuộc hội thoại (xem ví dụ
summarize.ts) - Các công cụ tương tác (câu hỏi, hướng dẫn, hộp thoại tùy chỉnh)
- Các công cụ trạng thái (danh sách việc cần làm, nhóm kết nối)
- Tích hợp bên ngoài (trình theo dõi tệp, webhooks, trình kích hoạt CI)
- Trò chơi trong khi chờ đợi (xem ví dụ
snake.ts)
Xem examples/extensions/ để biết cách triển khai hoạt động.
Mục lục
- 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
Bắt đầu nhanh
Tạo ~/.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");
},
});
}Kiểm tra bằng cờ --extension (hoặc -e):
pi -e ./my-extension.tsVị trí tiện ích mở rộng
Bảo mật: Extensions chạy với toàn bộ quyền hệ thống của bạn và có thể thực thi mã tùy ý. Chỉ cài đặt từ các nguồn mà bạn tin tưởng.
Extensions được tự động phát hiện từ các vị trí đáng tin cậy. Các mục nhập dự án .pi/extensions cục bộ chỉ tải sau khi dự án được tin cậy.
| Vị trí | Phạm vi |
|---|---|
~/.pi/agent/extensions/*.ts |
Toàn cầu (tất cả các dự án) |
~/.pi/agent/extensions/*/index.ts |
Toàn cầu (thư mục con) |
.pi/extensions/*.ts |
Dự án-địa phương |
.pi/extensions/*/index.ts |
Dự án-cục bộ (thư mục con) |
Đường dẫn bổ sung qua 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"
]
}Để chia sẻ tiện ích mở rộng qua npm hoặc git dưới dạng gói pi, hãy xem packages.md.
Nhập khẩu có sẵn
| Bưu kiện | Mục đích |
|---|---|
@earendil-works/pi-coding-agent |
Các loại tiện ích mở rộng (ExtensionAPI, ExtensionContext, sự kiện) |
typebox |
Định nghĩa lược đồ cho các tham số công cụ |
@earendil-works/pi-ai |
Tiện ích AI (StringEnum dành cho các enum tương thích với Google) |
@earendil-works/pi-tui |
TUI thành phần để hiển thị tùy chỉnh |
npm phần phụ thuộc cũng hoạt động. Thêm package.json bên cạnh tiện ích mở rộng của bạn (hoặc trong thư mục mẹ), chạy npm install và quá trình nhập từ node_modules/ được giải quyết tự động.
Đối với các gói pi phân tán được cài đặt bằng pi install (npm hoặc git), thời gian chạy phải ở dependencies. Cài đặt gói sử dụng cài đặt sản xuất (npm install --omit=dev) theo mặc định, vì vậy devDependencies không có sẵn trong thời gian chạy; khi npmCommand được định cấu hình, các gói git sử dụng install đơn giản để tương thích với các trình bao bọc.
Node.js tích hợp sẵn (node:fs, node:path, v.v.) cũng có sẵn.
Viết một phần mở rộng
Tiện ích mở rộng xuất hàm xuất xưởng mặc định nhận ExtensionAPI. Nhà máy có thể đồng bộ hoặc không đồng bộ:
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 được tải qua jiti, vì vậy TypeScript hoạt động mà không cần biên dịch.
Nếu nhà máy trả về Promise, pi sẽ đợi nó trước khi tiếp tục khởi động. Điều đó có nghĩa là quá trình khởi tạo không đồng bộ hoàn tất trước session_start, trước resources_discover và trước khi đăng ký nhà cung cấp xếp hàng qua pi.registerProvider() bị xóa.
Chức năng của nhà máy không đồng bộ
Sử dụng nhà máy không đồng bộ cho công việc khởi động một lần, chẳng hạn như tìm nạp cấu hình từ xa hoặc tự động khám phá các mô hình có sẵn.
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,
})),
});
}Mẫu này làm cho các mô hình được tìm nạp có sẵn trong quá trình khởi động bình thường và tới pi --list-models.
Tài nguyên tồn tại lâu dài và tắt máy
Các nhà máy mở rộng có thể chạy trong các lệnh gọi không bao giờ bắt đầu phiên. Không khởi động các tài nguyên nền như quy trình, ổ cắm, trình theo dõi tệp hoặc bộ hẹn giờ từ nhà máy.
Trì hoãn khởi động tài nguyên nền cho đến session_start hoặc lệnh/công cụ/sự kiện cần tài nguyên. Đăng ký trình xử lý session_shutdown bình thường để đóng mọi tài nguyên trong phạm vi phiên mà bạn bắt đầu.
Kiểu mở rộng
Tệp đơn - đơn giản nhất, dành cho các phần mở rộng nhỏ:
~/.pi/agent/extensions/
└── my-extension.tsThư mục có index.ts - dành cho phần mở rộng nhiều tệp:
~/.pi/agent/extensions/
└── my-extension/
├── index.ts # Entry point (exports default function)
├── tools.ts # Helper module
└── utils.ts # Helper moduleGói có phần phụ thuộc - dành cho các tiện ích mở rộng cần gói npm:
~/.pi/agent/extensions/
└── my-extension/
├── package.json # Declares dependencies and entry points
├── package-lock.json
├── node_modules/ # After npm install
└── src/
└── index.ts// package.json
{
"name": "my-extension",
"dependencies": {
"zod": "^3.0.0",
"chalk": "^5.0.0"
},
"pi": {
"extensions": ["./src/index.ts"]
}
}Chạy npm install trong thư mục tiện ích mở rộng, sau đó nhập từ node_modules/ sẽ tự động hoạt động.
Sự kiện
Tổng quan về vòng đời
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_shutdownSự kiện khởi nghiệp
dự án_trust
Được kích hoạt trước khi pi quyết định có tin cậy một dự án có cấu hình động hay không (.pi hoặc .agents/skills). Nó chạy trong khi khởi động và khi thay thế phiên (ví dụ /resume) nhập vào một cwd mà sự tin cậy chưa được giải quyết trong quy trình hiện tại. Chỉ có tiện ích mở rộng người dùng/toàn cầu và tiện ích mở rộng CLI -e mới tham gia; tiện ích mở rộng dự án cục bộ không được tải cho đến khi giải quyết được sự tin cậy.
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" };
});Trình xử lý project_trust phải trả về { trusted: "yes" | "no" | "undecided" }. Tiện ích mở rộng người dùng/toàn cầu hoặc CLI trả về "yes" hoặc "no" sở hữu quyết định; quyết định có/không đầu tiên sẽ thắng và loại bỏ lời nhắc tin cậy tích hợp. Sử dụng remember: true để duy trì quyết định có/không; nếu không nó chỉ áp dụng cho quy trình hiện tại. Trả về "undecided" để cho phép các trình xử lý sau hoặc luồng tin cậy tích hợp quyết định. Kiểm tra ctx.hasUI trước khi nhắc. Nếu không có trình xử lý nào trả về có/không, quá trình phân giải tin cậy thông thường sẽ tiếp tục: các quyết định trust.json đã lưu được áp dụng trước, sau đó defaultProjectTrust kiểm soát xem pi yêu cầu, tin tưởng hay từ chối theo mặc định.
Sự kiện tài nguyên
tài nguyên_khám phá
Được kích hoạt sau session_start để tiện ích mở rộng có thể đóng góp thêm đường dẫn kỹ năng, lời nhắc và chủ đề.
Đường dẫn khởi động sử dụng reason: "startup". Tải lại sử dụng 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"],
};
});Sự kiện phiên
Xem Session Format để biết nội dung lưu trữ phiên và Trình quản lý phiên API.
phiên_bắt đầu
Được kích hoạt khi một phiên được bắt đầu, tải hoặc tải lại.
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");
});phiên_info_changed
Được kích hoạt khi tên hiển thị phiên hiện tại được đặt qua /name, RPC hoặc 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");
});phiên_trước_switch
Được kích hoạt trước khi bắt đầu phiên mới (/new) hoặc chuyển đổi phiên (/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 };
}
});Sau khi chuyển đổi thành công hoặc hành động phiên mới, pi phát ra session_shutdown cho phiên bản tiện ích mở rộng cũ, tải lại và liên kết lại các tiện ích mở rộng cho phiên mới, sau đó phát ra session_start với reason: "new" | "resume" và previousSessionFile.
Thực hiện công việc dọn dẹp trong session_shutdown, sau đó thiết lập lại mọi trạng thái trong bộ nhớ trong session_start.
phiên_trước_fork
Được kích hoạt khi phân nhánh qua /fork hoặc sao chép qua /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
});Sau khi phân nhánh hoặc sao chép thành công, pi phát ra session_shutdown cho phiên bản tiện ích mở rộng cũ, tải lại và liên kết lại các tiện ích mở rộng cho phiên mới, sau đó phát ra session_start với reason: "fork" và previousSessionFile.
Thực hiện công việc dọn dẹp trong session_shutdown, sau đó thiết lập lại mọi trạng thái trong bộ nhớ trong session_start.
session_b Before_compact / session_compact
Bắn vào nén. Xem compaction.md để biết chi tiết.
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)
});phiên_trước_tree / session_tree
Đã kích hoạt trên điều hướng /tree. Xem Sessions để biết các khái niệm điều hướng cây.
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
});phiên_tắt máy
Được kích hoạt trước khi thời gian chạy phiên bắt đầu bị phá bỏ. Sử dụng tính năng này để dọn sạch các tài nguyên được mở từ session_start hoặc các hook trong phạm vi phiên khác.
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.
});Sự kiện đại lý
before_agent_start
Được kích hoạt sau khi người dùng gửi lời nhắc, trước vòng lặp tác nhân. Có thể đưa vào một tin nhắn và/hoặc sửa đổi lời nhắc hệ thống.
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...",
};
});Trường systemPromptOptions cấp cho tiện ích mở rộng quyền truy cập vào cùng dữ liệu có cấu trúc Pi sử dụng để xây dựng lời nhắc hệ thống. Điều này cho phép bạn kiểm tra những gì Pi đã tải — lời nhắc tùy chỉnh, nguyên tắc, đoạn mã công cụ, context files, kỹ năng — mà không cần khám phá lại tài nguyên hoặc phân tích lại cờ. Hãy sử dụng tính năng này khi tiện ích mở rộng của bạn cần thực hiện các thay đổi sâu sắc và đầy đủ thông tin đối với lời nhắc của hệ thống trong khi vẫn tôn trọng cấu hình do người dùng cung cấp.
Bên trong before_agent_start, event.systemPrompt và ctx.getSystemPrompt() đều phản ánh lời nhắc hệ thống được xâu chuỗi như của trình xử lý hiện tại. Trình xử lý before_agent_start sau này vẫn có thể sửa đổi lại.
đại lý_bắt đầu / đại lý_end / đại lý_settled
agent_start kích hoạt khi quá trình chạy tác nhân cấp thấp bắt đầu. agent_end kích hoạt khi quá trình chạy đó kết thúc, nhưng Pi vẫn có thể tự động thử lại, tự động nén và thử lại hoặc tiếp tục với các tin nhắn tiếp theo được xếp hàng đợi. Sử dụng agent_settled để tích hợp trạng thái cần biết Pi sẽ không tự động chạy tiếp.
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.
});lượt_bắt đầu / lượt_kết thúc
Được kích hoạt cho mỗi lượt (một phản hồi LLM + lệnh gọi công cụ).
pi.on("turn_start", async (event, ctx) => {
// event.turnIndex, event.timestamp
});
pi.on("turn_end", async (event, ctx) => {
// event.turnIndex, event.message, event.toolResults
});tin nhắn_bắt đầu / tin nhắn_update / tin nhắn_end
Được kích hoạt để cập nhật vòng đời tin nhắn.
message_startvàmessage_endkích hoạt thông báo kết quả của người dùng, trợ lý và công cụ.message_updatekích hoạt các bản cập nhật phát trực tuyến của trợ lý.- Trình xử lý
message_endcó thể trả về{ message }để thay thế thông báo cuối cùng. Việc thay thế phải giữ nguyênrole.
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
Được kích hoạt để cập nhật vòng đời thực thi công cụ.
Ở chế độ công cụ song song:
tool_execution_startđược phát ra theo thứ tự nguồn trợ lý trong giai đoạn chuẩn bị chiếu sángtool_execution_updatesự kiện có thể xen kẽ giữa các công cụtool_execution_endđược phát ra theo thứ tự hoàn thành công cụ sau khi mỗi công cụ được hoàn thiện- sự kiện tin nhắn
toolResultcuối cùng vẫn được phát sau theo thứ tự nguồn trợ lý
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
});bối cảnh
Được kích hoạt trước mỗi cuộc gọi LLM. Sửa đổi tin nhắn không phá hủy. Xem Session Format để biết các loại tin nhắn.
pi.on("context", async (event, ctx) => {
// event.messages - deep copy, safe to modify
const filtered = event.messages.filter(m => !shouldPrune(m));
return { messages: filtered };
});before_provider_headers
Được kích hoạt sau khi các tiêu đề HTTP gửi đi được tập hợp. Sử dụng nó để thêm, ghi đè hoặc xóa tiêu đề yêu cầu.
Trình xử lý biến đổi event.headers tại chỗ. Đặt khóa thành một chuỗi để thêm hoặc ghi đè chuỗi đó hoặc thành null để xóa chuỗi đó.
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;
});Chạy một lần theo yêu cầu của nhà cung cấp; thử lại sử dụng lại các tiêu đề tương tự thay vì kích hoạt lại hook.
before_provider_request
Được kích hoạt sau khi tải trọng dành riêng cho nhà cung cấp được tạo, ngay trước khi yêu cầu được gửi. Trình xử lý chạy theo thứ tự tải mở rộng. Việc trả về undefined giữ cho tải trọng không thay đổi. Trả về bất kỳ giá trị nào khác sẽ thay thế tải trọng cho các trình xử lý sau này và cho yêu cầu thực tế.
Móc này có thể viết lại hướng dẫn hệ thống cấp nhà cung cấp hoặc xóa chúng hoàn toàn. Những thay đổi ở cấp tải trọng đó không được phản ánh bởi ctx.getSystemPrompt(), báo cáo chuỗi lời nhắc hệ thống của Pi thay vì tải trọng cuối cùng của nhà cung cấp được tuần tự hóa.
pi.on("before_provider_request", (event, ctx) => {
console.log(JSON.stringify(event.payload, null, 2));
// Optional: replace payload
// return { ...event.payload, temperature: 0 };
});Điều này chủ yếu hữu ích để gỡ lỗi hành vi tuần tự hóa và bộ đệm của nhà cung cấp.
after_provider_response
Được kích hoạt sau khi nhận được phản hồi HTTP và trước khi nội dung luồng của nó được sử dụng. Trình xử lý chạy theo thứ tự tải mở rộng.
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"]);
}
});Tính khả dụng của tiêu đề phụ thuộc vào nhà cung cấp và phương tiện vận chuyển. Providers rằng phản hồi HTTP trừu tượng có thể không hiển thị tiêu đề.
Sự kiện mẫu
model_select
Được kích hoạt khi mô hình thay đổi thông qua lệnh /model, xoay vòng mô hình (Ctrl+P) hoặc khôi phục phiên.
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");
});Sử dụng tính năng này để cập nhật các thành phần giao diện người dùng (thanh trạng thái, chân trang) hoặc thực hiện khởi tạo theo mô hình cụ thể khi mô hình hoạt động thay đổi.
suy nghĩ_level_select
Bắn khi mức độ suy nghĩ thay đổi. Đây chỉ là thông báo; giá trị trả về của trình xử lý bị bỏ qua.
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}`);
});Sử dụng tính năng này để cập nhật giao diện người dùng mở rộng khi pi.setThinkingLevel(), thay đổi mô hình hoặc các điều khiển cấp độ tư duy tích hợp thay đổi cấp độ tư duy tích cực.
Sự kiện công cụ
tool_call
Được kích hoạt sau tool_execution_start, trước khi công cụ thực thi. Có thể chặn. Sử dụng isToolCallEventType để thu hẹp và nhận dữ liệu nhập đã nhập.
Trước khi tool_call chạy, pi đợi các sự kiện Tác nhân được phát ra trước đó hoàn tất việc tiêu hao thông qua AgentSession. Điều này có nghĩa là ctx.sessionManager được cập nhật thông qua tin nhắn gọi công cụ trợ lý hiện tại.
Trong chế độ thực thi công cụ song song mặc định, các lệnh gọi công cụ anh chị em từ cùng một thông báo trợ lý sẽ được chiếu trước một cách tuần tự, sau đó được thực thi đồng thời. tool_call không đảm bảo sẽ thấy kết quả của công cụ tương tự từ cùng một thông báo trợ lý đó trong ctx.sessionManager.
event.input có thể thay đổi. Thay đổi nó tại chỗ để vá các đối số của công cụ trước khi thực thi.
Hành vi đảm bảo:
- Các đột biến đối với
event.inputảnh hưởng đến việc thực thi công cụ thực tế - Trình xử lý
tool_callsau này sẽ thấy các đột biến do trình xử lý trước đó thực hiện - Không có xác nhận lại được thực hiện sau khi đột biến của bạn
- Trả về các giá trị từ việc chặn kiểm soát
tool_callthông qua{ block: true, reason?: string, terminate?: boolean } terminatechỉ áp dụng cho cuộc gọi bị chặn; tác nhân chỉ dừng sớm khi mọi kết quả cuối cùng trong đợt chấm dứt
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}`);
}
});Nhập công cụ tùy chỉnh
Các công cụ tùy chỉnh sẽ xuất loại đầu vào của chúng:
// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;Sử dụng isToolCallEventType với các tham số loại rõ ràng:
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
}
});công cụ_kết quả
Được kích hoạt sau khi quá trình thực thi công cụ kết thúc và trước tool_execution_end cộng với các sự kiện thông báo kết quả công cụ cuối cùng được phát ra. Có thể sửa đổi kết quả.
Ở chế độ công cụ song song, tool_result và tool_execution_end có thể xen kẽ theo thứ tự hoàn thành công cụ, trong khi các sự kiện thông báo toolResult cuối cùng vẫn được phát ra sau đó theo thứ tự nguồn trợ lý.
tool_result chuỗi xử lý giống như phần mềm trung gian:
- Trình xử lý chạy theo thứ tự tải mở rộng
- Mỗi trình xử lý nhìn thấy kết quả mới nhất sau khi thay đổi trình xử lý trước đó
- Trình xử lý có thể trả về các bản vá một phần (
content,details,isErrorhoặcusage); các trường bị bỏ qua giữ nguyên giá trị hiện tại của chúng
Sử dụng ctx.signal cho công việc không đồng bộ lồng nhau bên trong trình xử lý. Điều này cho phép Esc hủy lệnh gọi mô hình, fetch() và các hoạt động nhận biết hủy bỏ khác do tiện ích mở rộng bắt đầu.
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 };
});Sự kiện Bash của người dùng
người dùng_bash
Được kích hoạt khi người dùng thực thi lệnh ! hoặc !!. Có thể chặn.
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 } };
});Sự kiện đầu vào
đầu vào
Được kích hoạt khi nhận được đầu vào của người dùng, sau khi các lệnh mở rộng được kiểm tra nhưng trước khi mở rộng kỹ năng và mẫu. Sự kiện nhìn thấy văn bản đầu vào thô, vì vậy /skill:foo và /template chưa được mở rộng.
Xử lý đơn hàng:
- Các lệnh mở rộng (
/cmd) được chọn trước - nếu tìm thấy, trình xử lý sẽ chạy và sự kiện đầu vào bị bỏ qua inputcháy sự kiện - có thể chặn, biến đổi hoặc xử lý- Nếu không được xử lý: lệnh kỹ năng (
/skill:name) sẽ mở rộng sang nội dung kỹ năng - Nếu không được xử lý: prompt templates (
/template) được mở rộng sang nội dung mẫu - Quá trình xử lý tác nhân bắt đầu (
before_agent_start, v.v.)
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
});Kết quả:
continue- chuyển qua không thay đổi (mặc định nếu trình xử lý không trả về gì)transform- sửa đổi văn bản/hình ảnh, sau đó tiếp tục mở rộnghandled- bỏ qua hoàn toàn tác nhân (người xử lý đầu tiên trả lại chiến thắng này)
Chuyển đổi chuỗi trên các trình xử lý. Xem input-transform.ts và input-transform-streaming.ts để biết định tuyến nhận biết streamingBehavior.
Bối cảnh mở rộng
Tất cả người xử lý đều nhận được ctx: ExtensionContext.
ctx.ui
Phương thức UI để tương tác với người dùng. Xem Custom UI để biết chi tiết đầy đủ.
ctx.mode
Chế độ chạy hiện tại: "tui", "rpc", "json" hoặc "print". Sử dụng ctx.mode === "tui" để bảo vệ các tính năng chỉ dành cho thiết bị đầu cuối như custom(), nhà máy sản xuất thành phần, đầu vào thiết bị đầu cuối và hiển thị TUI trực tiếp.
ctx.hasUI
true ở chế độ TUI và RPC. false ở chế độ in (-p) và chế độ JSON. Sử dụng tính năng này để bảo vệ các phương thức hộp thoại (select, confirm, input, editor) và các phương thức bắn và quên (notify, setStatus, setWidget, setTitle, setEditorText) hoạt động ở cả TUI và chế độ RPC. Trong chế độ RPC, một số phương pháp dành riêng cho TUI là không hoạt động hoặc trả về giá trị mặc định (xem rpc.md).
ctx.cwd
Thư mục làm việc hiện tại.
Sử dụng CONFIG_DIR_NAME thay vì mã hóa cứng .pi khi xây dựng đường dẫn cấu hình cục bộ dự án. Các bản phân phối được đổi thương hiệu có thể sử dụng tên thư mục cấu hình khác.
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()
Trả về xem độ tin cậy của dự án cục bộ có hoạt động trong bối cảnh phiên hiện tại hay không. Điều này bao gồm các quyết định tin cậy tạm thời và CLI ghi đè tin cậy, không chỉ các quyết định đã lưu trong kho ủy thác toàn cầu.
Sử dụng điều này trước khi đọc cấu hình tiện ích mở rộng cục bộ của dự án chỉ nên được tôn trọng đối với các dự án đáng tin cậy.
ctx.sessionManager
Quyền truy cập chỉ đọc vào trạng thái phiên. Xem Session Format để biết đầy đủ SessionManager API và các loại mục nhập.
Đối với tool_call, trạng thái này được đồng bộ hóa thông qua thông báo trợ lý hiện tại trước khi trình xử lý chạy. Trong chế độ thực thi công cụ song song, vẫn không đảm bảo bao gồm các kết quả của công cụ anh chị em từ cùng một thông báo trợ lý.
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
Truy cập vào các mô hình, nhà cung cấp và xác thực được giải quyết. ctx.modelRegistry.getProvider(id) trả về nhà cung cấp pi-ai hiệu quả, trong khi getProviderAuth(id) giải quyết API key, tiêu đề, URL cơ sở và môi trường trong phạm vi nhà cung cấp hiện tại mà không yêu cầu mô hình được tải. ctx.model là mô hình hoạt động và ctx.thinkingLevel là mức độ tư duy hiệu quả hiện tại.
ctx.scopedModels là danh sách các mô hình chỉ đọc trong phạm vi phiên hiện tại — giống như tập hợp mà lệnh /scoped-models hiển thị. Vấn đề này được giải quyết khi bắt đầu phiên từ cờ --models CLI và cài đặt enabledModels (khớp với danh mục có sẵn với trận đấu nhỏ trên provider/modelId hoặc trần modelId). Nó trống khi không định cấu hình phạm vi, nghĩa là mọi mô hình có sẵn đều có thể sử dụng được. Mỗi mục nhập là { model, thinkingLevel? }, trong đó thinkingLevel chỉ được đặt khi một mẫu đã ghim nó (ví dụ: anthropic/*:high). Sử dụng nó để điền vào bộ chọn mô hình phản ánh bộ chọn tích hợp thay vì liệt kê toàn bộ danh mục thông qua ctx.modelRegistry.getAvailable().
ctx.signal
Tín hiệu hủy tác nhân hiện tại hoặc undefined khi không có lượt tác nhân nào được kích hoạt.
Sử dụng công cụ này để hủy bỏ công việc lồng nhau được bắt đầu bởi trình xử lý tiện ích mở rộng, ví dụ:
fetch(..., { signal: ctx.signal })- cuộc gọi mô hình chấp nhận
signal - người trợ giúp tệp hoặc quy trình chấp nhận
AbortSignal
ctx.signal thường được xác định trong các sự kiện lượt đang hoạt động như tool_call, tool_result, message_update và turn_end.
Nó thường là undefined trong các bối cảnh nhàn rỗi hoặc không rẽ, chẳng hạn như sự kiện phiên, lệnh mở rộng và các phím tắt được kích hoạt khi pi không hoạt động.
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()
Kiểm soát người trợ giúp luồng. ctx.isIdle() là sai trong khi Pi đang xử lý một lần chạy tác nhân, thử lại tự động, thử lại tự động nén hoặc tiếp tục xếp hàng đợi.
ctx.shutdown()
Yêu cầu tắt pi một cách duyên dáng.
- Chế độ tương tác: Trì hoãn cho đến khi tổng đài viên không hoạt động (sau khi xử lý tất cả các tin nhắn chỉ đạo và theo dõi được xếp hàng đợi).
- RPC chế độ: Trì hoãn cho đến trạng thái không hoạt động tiếp theo (sau khi hoàn thành phản hồi lệnh hiện tại, khi chờ lệnh tiếp theo).
- Chế độ in: Không hoạt động. Quá trình sẽ tự động thoát khi tất cả lời nhắc được xử lý.
Phát ra sự kiện session_shutdown cho tất cả tiện ích mở rộng trước khi thoát. Có sẵn trong mọi ngữ cảnh (trình xử lý sự kiện, công cụ, lệnh, phím tắt).
pi.on("tool_call", (event, ctx) => {
if (isFatal(event.input)) {
ctx.shutdown();
}
});ctx.getContextUsage()
Trả về mức sử dụng ngữ cảnh hiện tại cho mô hình đang hoạt động. Sử dụng mức sử dụng trợ lý cuối cùng khi có sẵn, sau đó ước tính mã thông báo cho các tin nhắn theo dõi.
const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
// ...
}ctx.compact()
Kích hoạt nén mà không cần chờ hoàn thành. Sử dụng onComplete và onError cho các hành động tiếp theo.
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()
Trả về chuỗi lời nhắc hệ thống hiện tại của Pi.
- Trong
before_agent_start, điều này phản ánh những thay đổi theo chuỗi của hệ thống được thực hiện cho đến lượt hiện tại. - Nó không bao gồm các đột biến tin nhắn
contextsau này. - Nó không bao gồm việc ghi lại tải trọng
before_provider_request. - Nếu các tiện ích mở rộng được tải sau này chạy sau tiện ích mở rộng của bạn, chúng vẫn có thể thay đổi nội dung được gửi cuối cùng.
pi.on("before_agent_start", (event, ctx) => {
const prompt = ctx.getSystemPrompt();
console.log(`System prompt length: ${prompt.length}`);
});Phần mở rộngCommandContext
Trình xử lý lệnh nhận ExtensionCommandContext, mở rộng ExtensionContext bằng các phương thức kiểm soát phiên. Những điều này chỉ có trong lệnh vì chúng có thể gây bế tắc nếu được gọi từ trình xử lý sự kiện.
ctx.getSystemPromptOptions()
Trả về đầu vào cơ sở Pi hiện đang sử dụng để xây dựng lời nhắc hệ thống.
const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];Điều này có hình dạng và khả năng thay đổi tương tự như before_agent_start event.systemPromptOptions: lời nhắc tùy chỉnh, công cụ hoạt động, đoạn công cụ, hướng dẫn lời nhắc, văn bản lời nhắc hệ thống được nối thêm, cwd, đã tải context files và các kỹ năng đã tải. Nó có thể bao gồm nội dung tệp ngữ cảnh đầy đủ, vì vậy hãy coi nó như dữ liệu cục bộ mở rộng nhạy cảm và tránh hiển thị nó thông qua danh sách lệnh, nhật ký hoặc siêu dữ liệu tự động hoàn thành.
Điều này báo cáo các đầu vào nhắc cơ sở hiện tại. Nó không bao gồm các thay đổi nhắc nhở của hệ thống theo chuỗi before_agent_start mỗi lượt, các đột biến thông báo sự kiện context sau này hoặc ghi lại tải trọng before_provider_request.
ctx.waitForIdle()
Đợi tác nhân xử lý hoàn toàn, bao gồm thử lại tự động, thử lại tự động nén và tiếp tục xếp hàng:
pi.registerCommand("my-cmd", {
handler: async (args, ctx) => {
await ctx.waitForIdle();
// Agent is now idle, safe to modify session
},
});ctx.newSession(tùy chọn?)
Tạo một phiên mới:
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
}Tùy chọn:
parentSession: tệp phiên gốc để ghi vào tiêu đề phiên mớisetup: thay đổiSessionManagercủa phiên mới trước khiwithSessionchạywithSession: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụngpi/ lệnhctxcũ đã bắt; xem Session replacement lifecycle and footguns.
ctx.fork(entryId, tùy chọn?)
Ngã ba từ một mục cụ thể, tạo một tệp phiên mới:
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
}Tùy chọn:
position:"before"(mặc định) phân nhánh trước thông báo của người dùng đã chọn, khôi phục lời nhắc đó vào trình chỉnh sửaposition:"at"sao chép đường dẫn đang hoạt động thông qua mục đã chọn mà không khôi phục văn bản soạn thảowithSession: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụngpi/ lệnhctxcũ đã bắt; xem Session replacement lifecycle and footguns.
ctx.navigateTree(targetId, tùy chọn?)
Điều hướng đến một điểm khác trong 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",
});Tùy chọn:
summarize: Có tạo bản tóm tắt về nhánh bị bỏ rơi hay khôngcustomInstructions: Hướng dẫn tùy chỉnh cho phần tóm tắtreplaceInstructions: Nếu đúng,customInstructionsthay thế lời nhắc mặc định thay vì được thêm vàolabel: Nhãn để gắn vào mục tóm tắt nhánh (hoặc mục tiêu nếu không tóm tắt)
ctx.switchSession(sessionPath, tùy chọn?)
Chuyển sang một tệp phiên khác:
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
}Tùy chọn:
withSession: chạy công việc sau chuyển đổi dựa trên bối cảnh phiên thay thế mới. Không sử dụngpi/ lệnhctxcũ đã bắt; xem Session replacement lifecycle and footguns.
Để khám phá các phiên có sẵn, hãy sử dụng các phương thức tĩnh SessionManager.list() hoặc 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");
},
});
}
},
});Vòng đời thay thế phiên và súng ngắn
withSession nhận được ReplacedSessionContext mới, mở rộng ExtensionCommandContext với các trình trợ giúp không đồng bộ sendMessage() và sendUserMessage() được liên kết với phiên thay thế.
Vòng đời và súng ngắn:
withSessionchỉ chạy sau khi phiên cũ đã phát rasession_shutdown, thời gian chạy cũ đã bị phá bỏ, phiên thay thế đã được khôi phục và phiên bản tiện ích mở rộng mới đã nhận đượcsession_start.- Cuộc gọi lại vẫn thực thi trong bao đóng ban đầu, không phải bên trong phiên bản tiện ích mở rộng mới. Điều đó có nghĩa là phiên bản tiện ích mở rộng cũ của bạn có thể đã chạy quá trình dọn dẹp tắt máy trước khi
withSessionbắt đầu. - Các đối tượng cũ
pi/ lệnh cũctxđã bị bắt giữ sẽ cũ sau khi thay thế và sẽ bị loại bỏ nếu được sử dụng. Chỉ sử dụngctxđược chuyển đếnwithSessioncho công việc theo phiên. - Các đối tượng thô được trích xuất trước đó vẫn là trách nhiệm của bạn. Ví dụ: nếu bạn chụp
const sm = ctx.sessionManagertrước khi thay thế,smvẫn là đối tượngSessionManagercũ. Không sử dụng lại nó sau khi thay thế. - Mã trong
withSessionsẽ giả sử bất kỳ trạng thái nào bị trình xử lýsession_shutdowncủa bạn vô hiệu hóa đều đã biến mất. Chỉ thu thập dữ liệu đơn giản còn tồn tại sau khi tắt máy một cách sạch sẽ, chẳng hạn như chuỗi, id và cấu hình được tuần tự hóa.
Mô hình an toàn:
pi.registerCommand("handoff", {
handler: async (_args, ctx) => {
const kickoff = "Continue from the replacement session";
await ctx.newSession({
withSession: async (ctx) => {
await ctx.sendUserMessage(kickoff);
},
});
},
});Mẫu không an toàn:
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()
Chạy luồng tải lại tương tự như /reload.
pi.registerCommand("reload-runtime", {
description: "Reload extensions, skills, prompts, themes, and context files",
handler: async (_args, ctx) => {
await ctx.reload();
return;
},
});Hành vi quan trọng:
await ctx.reload()phát rasession_shutdowncho thời gian chạy tiện ích mở rộng hiện tại- Sau đó, nó tải lại tài nguyên và phát ra
session_startvớireason: "reload"vàresources_discovervới lý do"reload" - Trình xử lý lệnh hiện đang chạy vẫn tiếp tục trong khung cuộc gọi cũ
- Mã sau
await ctx.reload()vẫn chạy từ phiên bản tải lại trước - Mã sau
await ctx.reload()không được cho rằng trạng thái tiện ích mở rộng trong bộ nhớ cũ vẫn hợp lệ - Sau khi trình xử lý quay trở lại, các lệnh/sự kiện/cuộc gọi công cụ trong tương lai sẽ sử dụng phiên bản tiện ích mở rộng mới
Để có thể dự đoán hành vi, hãy coi tải lại là thiết bị đầu cuối cho trình xử lý đó (await ctx.reload(); return;).
Các công cụ chạy bằng ExtensionContext nên không thể gọi trực tiếp ctx.reload(). Sử dụng lệnh làm điểm nhập tải lại, sau đó hiển thị công cụ xếp hàng lệnh đó dưới dạng thông báo tiếp theo của người dùng.
Công cụ ví dụ mà LLM có thể gọi để kích hoạt tải lại:
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." }],
};
},
});
}Phương thức mở rộngAPI
pi.on(sự kiện, trình xử lý)
Đăng ký các sự kiện. Xem Events để biết loại sự kiện và giá trị trả về.
pi.registerTool(định nghĩa)
Đăng ký một công cụ tùy chỉnh có thể gọi được bởi LLM. Xem Custom Tools để biết chi tiết đầy đủ.
pi.registerTool() hoạt động cả trong khi tải tiện ích mở rộng và sau khi khởi động. Bạn có thể gọi nó bên trong session_start, trình xử lý lệnh hoặc các trình xử lý sự kiện khác. Các công cụ mới được làm mới ngay lập tức trong cùng một phiên, vì vậy chúng xuất hiện trong pi.getAllTools() và có thể được LLM gọi mà không cần /reload.
Sử dụng pi.setActiveTools() để bật hoặc tắt các công cụ (bao gồm cả các công cụ được thêm động) khi chạy.
Sử dụng promptSnippet để chọn một công cụ tùy chỉnh vào mục nhập một dòng trong Available tools và promptGuidelines để nối các dấu đầu dòng dành riêng cho công cụ vào phần Guidelines mặc định khi công cụ này đang hoạt động.
Quan trọng: promptGuidelines đầu đạn được gắn phẳng vào phần Guidelines không có tiền tố tên công cụ. Mỗi hướng dẫn phải đặt tên cho công cụ mà nó đề cập đến - tránh "Sử dụng công cụ này khi..." vì LLM không thể cho biết "cái này" nghĩa là công cụ nào. Thay vào đó hãy viết "Sử dụng my_tool khi...".
Xem dynamic-tools.ts để biết ví dụ đầy đủ.
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(tin nhắn, tùy chọn?)
Đưa một tin nhắn tùy chỉnh vào phiên. Thông báo tùy chỉnh tham gia vào ngữ cảnh LLM. Đối với nội dung lâu dài chỉ dành cho TUI không được gửi tới LLM, hãy sử dụng pi.appendEntry() với pi.registerEntryRenderer().
pi.sendMessage({
customType: "my-extension",
content: "Message text",
display: true,
details: { ... },
}, {
triggerTurn: true,
deliverAs: "steer",
});Tùy chọn:
deliverAs- Chế độ giao hàng:"steer"(mặc định) - Xếp hàng tin nhắn trong khi truyền phát. Được phân phối sau khi lượt trợ lý hiện tại hoàn tất việc thực hiện các lệnh gọi công cụ của nó, trước lệnh gọi LLM tiếp theo."followUp"- Chờ đại lý kết thúc. Chỉ được giao khi đại lý không còn cuộc gọi công cụ nào nữa."nextTurn"- Xếp hàng chờ lời nhắc của người dùng tiếp theo. Không làm gián đoạn hoặc kích hoạt bất cứ điều gì.
triggerTurn: true- Nếu tác nhân không hoạt động, hãy kích hoạt phản hồi LLM ngay lập tức. Chỉ áp dụng cho chế độ"steer"và"followUp"(bỏ qua đối với"nextTurn").
pi.sendUserMessage(nội dung, tùy chọn?)
Gửi tin nhắn người dùng đến đại lý. Không giống như sendMessage() gửi tin nhắn tùy chỉnh, thao tác này sẽ gửi một tin nhắn thực tế của người dùng xuất hiện như thể được người dùng nhập. Luôn kích hoạt một lượt.
// 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" });Tùy chọn:
deliverAs- Bắt buộc khi tác nhân đang phát trực tuyến:"steer"- Xếp hàng tin nhắn để gửi sau khi lượt trợ lý hiện tại thực hiện xong các lệnh gọi công cụ của mình"followUp"- Chờ đại lý làm xong hết công cụ
Khi không phát trực tuyến, tin nhắn sẽ được gửi ngay lập tức và kích hoạt một lượt mới. Khi phát trực tiếp mà không có deliverAs, sẽ xảy ra lỗi.
Xem send-user-message.ts để biết ví dụ đầy đủ.
pi.appendEntry(customType, dữ liệu?)
Kiên trì dữ liệu mở rộng. Các mục tùy chỉnh KHÔNG tham gia vào ngữ cảnh LLM. Ở chế độ tương tác, chúng cũng có thể hiển thị bên trong bản ghi cuộc trò chuyện khi được ghép nối với 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(name)
Đặt tên hiển thị phiên (hiển thị trong bộ chọn phiên thay vì tin nhắn đầu tiên).
pi.setSessionName("Refactor auth module");pi.getSessionName()
Lấy tên phiên hiện tại, nếu được đặt.
const name = pi.getSessionName();
if (name) {
console.log(`Session: ${name}`);
}pi.setLabel(entryId, nhãn)
Đặt hoặc xóa nhãn trên một mục nhập. Nhãn là các điểm đánh dấu do người dùng xác định để đánh dấu trang và điều hướng (hiển thị trong bộ chọn /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);Nhãn vẫn tồn tại trong phiên và tồn tại khi khởi động lại. Sử dụng chúng để đánh dấu các điểm quan trọng (điểm rẽ, điểm kiểm tra) trong cây hội thoại.
pi.registerCommand(tên, tùy chọn)
Đăng ký một lệnh.
Nếu nhiều tiện ích mở rộng đăng ký cùng một tên lệnh, pi sẽ giữ tất cả chúng và gán các hậu tố gọi số theo thứ tự tải, ví dụ: /review:1 và /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");
}
});Tùy chọn: thêm tính năng tự động hoàn thành đối số cho /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()
Nhận slash commands có sẵn để gọi thông qua prompt trong phiên hiện tại. Bao gồm các lệnh mở rộng, prompt templates và các lệnh kỹ năng.
Danh sách khớp với thứ tự RPC get_commands: đầu tiên là tiện ích mở rộng, sau đó là mẫu, sau đó là kỹ năng.
const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");Mỗi mục có hình dạng này:
{
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;
};
}Sử dụng sourceInfo làm trường xuất xứ chuẩn. Không suy ra quyền sở hữu từ tên lệnh hoặc từ phân tích cú pháp đường dẫn đặc biệt.
Các lệnh tương tác tích hợp (như /model và /settings) không được đưa vào đây. Chúng chỉ được xử lý trong tương tác
và sẽ không thực thi nếu được gửi qua prompt.
pi.registerMessageRenderer(customType, trình kết xuất)
Đăng ký trình kết xuất TUI tùy chỉnh cho các tin nhắn tùy chỉnh với customType của bạn. Thông báo tùy chỉnh được tạo bằng pi.sendMessage() và tham gia vào ngữ cảnh LLM. Xem Custom UI.
pi.registerMarkdownTransformer(máy biến áp)
Đăng ký một máy biến áp cho Markdown trong văn bản người dùng thông thường, văn bản trợ lý và khối tư duy. Máy biến áp chạy theo thứ tự tải mở rộng và mỗi máy biến áp nhận được Markdown do máy biến áp trước đó trả về. Sau khi chuỗi kết thúc, Pi hiển thị nội dung đã chuyển đổi bằng trình kết xuất tích hợp sẵn.
Máy biến áp nhận chuỗi Markdown và ngữ cảnh có:
messageType—"user","assistant"hoặc"assistant-thinking"isStreaming—trueđể cập nhật một phần trợ lý;falsedành cho người dùng, trợ lý đã hoàn thiện và tin nhắn được khôi phụcavailableWidth— các cột đầu cuối chính xác có sẵn cho nội dung Markdown được chuyển đổi
Trả về Markdown đã biến đổi:
pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
if (isStreaming || messageType === "assistant-thinking") return markdown;
return markdown.replaceAll("-->", "→");
});Nếu một máy biến áp bị hỏng, Pi sẽ giữ nguyên Markdown được tạo ra cho đến nay và tiếp tục với máy biến áp tiếp theo. Hook chỉ hiển thị: thông báo ban đầu không thay đổi trong bối cảnh phiên và mô hình. Nó chạy cho các tin nhắn của người dùng mới, các bản cập nhật phát trực tuyến của trợ lý, các tin nhắn phiên được khôi phục và các thay đổi về độ rộng của thiết bị đầu cuối, do đó, máy biến áp sẽ vẫn đồng bộ và không tốn kém.
pi.registerEntryRenderer(customType, trình kết xuất)
Đăng ký trình kết xuất TUI tùy chỉnh cho các mục tùy chỉnh bằng customType của bạn. Các mục nhập tùy chỉnh được tạo bằng pi.appendEntry() và không tham gia vào ngữ cảnh 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(phím tắt, tùy chọn)
Đăng ký một phím tắt. Xem keybindings.md để biết định dạng phím tắt và tổ hợp phím tích hợp.
pi.registerShortcut("ctrl+shift+p", {
description: "Toggle plan mode",
handler: async (ctx) => {
ctx.ui.notify("Toggled!");
},
});pi.registerFlag(tên, tùy chọn)
Đăng ký cờ CLI.
pi.registerFlag("plan", {
description: "Start in plan mode",
type: "boolean",
default: false,
});
// Check value
if (pi.getFlag("plan")) {
// Plan mode enabled
}pi.exec(lệnh, args, tùy chọn?)
Thực hiện lệnh shell.
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killedpi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Quản lý các công cụ đang hoạt động. Điều này hoạt động cho cả công cụ tích hợp và công cụ được đăng ký động. pi.getActiveTools() trả về tên công cụ đang hoạt động là string[]; pi.getAllTools() trả về siêu dữ liệu cho tất cả các công cụ được định cấu hình.
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() trả về name, description, parameters, promptGuidelines và sourceInfo.
Giá trị sourceInfo.source điển hình:
builtincho các công cụ tích hợpsdkđối với các công cụ được chuyển quacreateAgentSession({ customTools })- siêu dữ liệu nguồn mở rộng cho các công cụ được đăng ký bởi tiện ích mở rộng
pi.setModel(model)
Đặt mô hình hiện tại. Trả về false nếu không có sẵn API key cho mô hình. Xem models.md để biết cách định cấu hình các mô hình tùy chỉnh.
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(level)
Nhận hoặc thiết lập mức độ suy nghĩ. Mức độ được giới hạn theo khả năng của mô hình (các mô hình không suy luận luôn sử dụng "tắt"). Những thay đổi phát ra thinking_level_select.
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");pi.events
Bus sự kiện được chia sẻ để liên lạc giữa các tiện ích mở rộng:
pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });pi.registerProvider(tên, cấu hình)
Đăng ký hoặc ghi đè nhà cung cấp mô hình một cách linh hoạt. Hữu ích cho proxy, điểm cuối tùy chỉnh hoặc cấu hình mô hình toàn nhóm.
Các cuộc gọi được thực hiện trong chức năng của nhà máy mở rộng sẽ được xếp hàng đợi và được áp dụng sau khi trình chạy khởi tạo. Các lệnh gọi được thực hiện sau đó — ví dụ: từ trình xử lý lệnh theo luồng thiết lập của người dùng — sẽ có hiệu lực ngay lập tức mà không yêu cầu /reload.
Nhà cung cấp năng động có thể triển khai refreshModels. Pi gọi nó trong quá trình làm mới mô hình, xuất bản danh sách trả về một cách đồng bộ thông qua nhà cung cấp và chuyển ngữ cảnh thông tin xác thực chuẩn/danh mục được lưu trữ/mạng/tín hiệu. Tiện ích mở rộng quyết định xem có duy trì siêu dữ liệu danh mục thông qua context.publish({ persist: entry }) được kiểm tra thế hệ hay không; các máy chủ trực tiếp như llama.cpp có thể trả về các mô hình mà không cần lưu giữ chúng.
context.signal luôn là một tín hiệu cụ thể và lệnh gọi lại của nhà cung cấp phải chuyển tín hiệu đó sang chặn I/O. Các lệnh gọi ModelRuntime.refresh() và ModelRegistry.refresh() công khai chấp nhận tín hiệu tùy chọn và không bị chặn khi nó bị bỏ qua; tiện ích mở rộng và ứng dụng chọn thời hạn riêng của họ. Việc hủy sẽ khiến người gọi không phải chờ đợi ngay cả khi nhà cung cấp bỏ qua tín hiệu, nhưng vẫn cần phải hợp tác để dừng công việc cơ bản.
Extensions cần hành vi xác thực, lọc, làm mới hoặc truyền phát của nhà cung cấp gốc có thể đăng ký Provider hoàn chỉnh từ @earendil-works/pi-ai. Nhà cung cấp trở thành cơ sở tổng hợp và phần ghi đè models.json vẫn áp dụng ở trên nó.
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;
}
}
});Biểu mẫu đối tượng chấp nhận một pi-ai Provider hoàn chỉnh, bao gồm hành vi gốc auth, getModels, refreshModels, filterModels, stream và streamSimple.
Tùy chọn cấu hình cũ:
name- Tên hiển thị của nhà cung cấp trong giao diện người dùng, chẳng hạn như/login.baseUrl- API URL điểm cuối. Cần thiết khi xác định mô hình.apiKey- API key theo nghĩa đen, nội suy môi trường ($ENV_VARhoặc${ENV_VAR}) hoặc dẫn đầu!command. Bắt buộc khi xác định mô hình (trừ khi được cung cấpoauth).$thoát ``apiKey- API key theo nghĩa đen, nội suy môi trường ($ENV_VARhoặc${ENV_VAR}) hoặc dẫn đầu!command. Bắt buộc khi xác định mô hình (trừ khi được cung cấpoauth).$thoát và$!thoát!` theo nghĩa đen mà không kích hoạt thực thi lệnh.api- API gõ:"anthropic-messages","openai-completions","openai-responses", v.v.headers- Tiêu đề tùy chỉnh để đưa vào yêu cầu.authHeader- Nếu đúng, tự động thêm tiêu đềAuthorization: Bearer.models- Mảng định nghĩa mô hình. Nếu được cung cấp, sẽ thay thế tất cả các mô hình hiện có cho nhà cung cấp này. Định nghĩa mô hình có thể đặtbaseUrlđể ghi đè điểm cuối của nhà cung cấp cho mô hình đó.refreshModels- Gọi lại khám phá động không đồng bộ. Các mô hình được trả về của nó sẽ thay thế các mô hình do tiện ích mở rộng cung cấp.context.storedchứa ảnh chụp nhanh của nhà cung cấp liên tục; chỉ sử dụngcontext.publish({ persist: entry })được kiểm tra thế hệ khi dữ liệu danh mục đã cập nhật vẫn tồn tại. Sử dụngpersist: nullđể xóa ảnh chụp nhanh đó.oauth- Cấu hình nhà cung cấp OAuth để hỗ trợ/login. Khi được cung cấp, nhà cung cấp sẽ xuất hiện trong menu đăng nhập.streamSimple- Triển khai phát trực tuyến tùy chỉnh cho APIs không chuẩn.
Xem custom-provider.md để biết các chủ đề nâng cao: phát trực tuyến tùy chỉnh APIs, OAuth chi tiết, tham khảo định nghĩa mô hình.
pi.unregisterProvider(tên)
Xóa nhà cung cấp đã đăng ký trước đó và các mô hình của nhà cung cấp đó. Các mô hình tích hợp đã bị nhà cung cấp ghi đè sẽ được khôi phục. Không có hiệu lực nếu nhà cung cấp chưa được đăng ký.
Giống như registerProvider, điều này có hiệu lực ngay lập tức khi được gọi sau giai đoạn tải ban đầu, do đó không cần phải có /reload.
pi.registerCommand("my-setup-teardown", {
description: "Remove the custom proxy provider",
handler: async (_args, _ctx) => {
pi.unregisterProvider("my-proxy");
},
});Quản lý nhà nước
Extensions có trạng thái nên lưu trữ nó trong kết quả công cụ details để được hỗ trợ phân nhánh phù hợp:
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
};
},
});
}Công cụ tùy chỉnh
Đăng ký các công cụ mà LLM có thể gọi qua pi.registerTool(). Các công cụ xuất hiện trong lời nhắc hệ thống và có thể có kết xuất tùy chỉnh.
Sử dụng promptSnippet cho mục nhập một dòng ngắn trong phần Available tools trong lời nhắc hệ thống mặc định. Nếu bị bỏ qua, các công cụ tùy chỉnh sẽ bị loại khỏi phần đó.
Sử dụng promptGuidelines để thêm dấu đầu dòng dành riêng cho công cụ vào phần lời nhắc hệ thống mặc định Guidelines. Những dấu đầu dòng này chỉ được đưa vào khi công cụ đang hoạt động (ví dụ: sau pi.setActiveTools([...])).
Quan trọng: promptGuidelines đầu đạn được gắn phẳng vào phần Guidelines mà không có tiền tố hoặc nhóm tên công cụ. Mỗi hướng dẫn phải đặt tên cho công cụ mà nó đề cập đến - tránh "Sử dụng công cụ này khi..." vì LLM không thể cho biết "cái này" nghĩa là công cụ nào. Thay vào đó hãy viết "Sử dụng my_tool khi...".
Lưu ý: Một số mô hình thật ngu ngốc và bao gồm tiền tố @ trong đối số đường chạy dao. Các công cụ tích hợp sẽ loại bỏ dấu @ ở đầu trước khi giải quyết các đường dẫn. Nếu công cụ tùy chỉnh của bạn chấp nhận một đường dẫn, hãy chuẩn hóa cả @ ở đầu.
Nếu công cụ tùy chỉnh của bạn thay đổi tệp, hãy sử dụng withFileMutationQueue() để nó tham gia vào cùng một hàng đợi cho mỗi tệp như edit và write tích hợp sẵn. Điều này quan trọng vì các lệnh gọi công cụ chạy song song theo mặc định. Không có hàng đợi, hai công cụ có thể đọc cùng một nội dung tệp cũ, tính toán các bản cập nhật khác nhau và sau đó bất kỳ vùng ghi nào cuối cùng sẽ ghi đè lên vùng kia.
Trường hợp lỗi ví dụ: công cụ tùy chỉnh của bạn chỉnh sửa foo.ts trong khi edit tích hợp cũng thay đổi foo.ts trong cùng một lượt trợ lý. Nếu công cụ của bạn không tham gia vào hàng đợi, cả hai đều có thể đọc foo.ts gốc, áp dụng các thay đổi riêng biệt và một trong những thay đổi đó sẽ bị mất.
Chuyển đường dẫn tệp đích thực tới withFileMutationQueue() chứ không phải đối số người dùng thô. Trước tiên hãy giải quyết nó thành đường dẫn tuyệt đối, liên quan đến ctx.cwd hoặc thư mục làm việc của công cụ của bạn. Đối với các tệp hiện có, trình trợ giúp chuẩn hóa thông qua realpath(), do đó, các bí danh liên kết tượng trưng cho cùng một tệp sẽ chia sẻ một hàng đợi. Đối với các tệp mới, nó sẽ quay trở lại đường dẫn tuyệt đối đã được giải quyết vì chưa có gì cho realpath().
Xếp hàng toàn bộ cửa sổ đột biến trên đường dẫn đích đó. Điều đó bao gồm logic đọc-sửa-ghi, không chỉ là lần ghi cuối cùng.
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: {},
};
});
}Định nghĩa công cụ
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) { ... },
});Tính toán mức sử dụng: Nếu một công cụ thực hiện lệnh gọi LLM lồng nhau, hãy trả về Usage kết hợp của chúng dưới dạng usage. Pi duy trì nó trên kết quả của công cụ và đưa nó vào tổng số phiên ở chân trang, /session và RPC. tool_result trình xử lý có thể kiểm tra hoặc thay thế giá trị này.
Lỗi báo hiệu: Để đánh dấu việc thực thi công cụ là không thành công (đặt isError: true trên kết quả và báo cáo cho LLM), hãy gửi lỗi từ execute. Trả về một giá trị không bao giờ đặt cờ lỗi bất kể bạn đưa thuộc tính nào vào đối tượng trả về.
Chấm dứt sớm: Trả về terminate: true từ execute() để gợi ý rằng cuộc gọi LLM tiếp theo tự động sẽ bị bỏ qua sau đợt công cụ hiện tại. Điều này chỉ có hiệu lực khi mọi công cụ hoàn thiện trong lô đó đều chấm dứt. Xem examples/extensions/structured-output.ts để biết ví dụ tối thiểu trong đó tác nhân kết thúc bằng lệnh gọi công cụ đầu ra có cấu trúc cuối cùng.
// 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: {} };
}Quan trọng: Sử dụng StringEnum từ @earendil-works/pi-ai cho enum chuỗi. Type.Union/Type.Literal không hoạt động với API của Google.
Chuẩn bị lập luận: prepareArguments(args) là tùy chọn. Nếu được xác định, nó sẽ chạy trước xác thực lược đồ và trước execute(). Sử dụng nó để bắt chước hình dạng đầu vào cũ hơn được chấp nhận khi pi tiếp tục phiên cũ hơn có các đối số lệnh gọi công cụ được lưu trữ không còn khớp với lược đồ hiện tại. Trả về đối tượng bạn muốn xác thực dựa trên parameters. Giữ lược đồ công khai nghiêm ngặt. Không thêm các trường tương thích không được dùng nữa vào parameters chỉ để giữ cho các phiên cũ được tiếp tục hoạt động.
Ví dụ: phiên cũ hơn có thể chứa lệnh gọi công cụ edit với oldText và newText cấp cao nhất, trong khi lược đồ hiện tại chỉ chấp nhận 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: {},
};
},
});Ghi đè các công cụ tích hợp
Extensions có thể ghi đè các công cụ tích hợp (read, bash, edit, write, grep, find, ls) bằng cách đăng ký một công cụ có cùng tên. Chế độ tương tác hiển thị cảnh báo khi điều này xảy ra.
# Extension's read tool replaces built-in read
pi -e ./tool-override.tsNgoài ra, hãy sử dụng --no-builtin-tools để bắt đầu mà không cần bất kỳ công cụ tích hợp nào trong khi vẫn bật các công cụ mở rộng:
# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.tsXem examples/extensions/tool-override.ts để biết ví dụ hoàn chỉnh ghi đè read bằng kiểm soát ghi nhật ký và truy cập.
Kết xuất: Tính kế thừa của trình kết xuất tích hợp được giải quyết trên mỗi vị trí. Ghi đè thực thi và ghi đè kết xuất là độc lập. Nếu phần ghi đè của bạn bỏ qua renderCall thì renderCall tích hợp sẽ được sử dụng. Nếu phần ghi đè của bạn bỏ qua renderResult thì renderResult tích hợp sẽ được sử dụng. Nếu phần ghi đè của bạn bỏ qua cả hai thì trình kết xuất tích hợp sẵn sẽ tự động được sử dụng (đánh dấu cú pháp, khác biệt, v.v.). Điều này cho phép bạn bao bọc các công cụ tích hợp để ghi nhật ký hoặc kiểm soát quyền truy cập mà không cần triển khai lại giao diện người dùng.
Siêu dữ liệu nhắc nhở: promptSnippet và promptGuidelines không được kế thừa từ công cụ tích hợp sẵn. Nếu phần ghi đè của bạn phải giữ lại các hướng dẫn nhắc nhở đó, hãy xác định chúng một cách rõ ràng trên phần ghi đè.
Việc triển khai của bạn phải khớp với hình dạng kết quả chính xác, bao gồm loại details. Logic phiên và giao diện người dùng phụ thuộc vào các hình dạng này để hiển thị và theo dõi trạng thái.
Triển khai công cụ tích hợp:
- read.ts -
ReadToolDetails - bash.ts -
BashToolDetails - edit.ts
- write.ts
- grep.ts -
GrepToolDetails - find.ts -
FindToolDetails - ls.ts -
LsToolDetails
Thực thi từ xa
Các công cụ tích hợp hỗ trợ các hoạt động có thể cắm để ủy quyền cho các hệ thống từ xa (SSH, vùng chứa, v.v.):
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);
},
});Giao diện hoạt động: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations
Đối với user_bash, các tiện ích mở rộng có thể sử dụng lại phần phụ trợ shell cục bộ của pi thông qua createLocalBashOperations() thay vì triển khai lại việc sinh sản quy trình cục bộ, phân giải shell và chấm dứt cây quy trình.
Công cụ bash cũng hỗ trợ spawn hook để điều chỉnh lệnh, cwd hoặc env trước khi thực thi:
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() hiển thị phiên hiện tại với các lệnh thông qua PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL và PI_REASONING_LEVEL. Việc tiêm xảy ra trước spawnHook, vì vậy hook nhận các giá trị này trong env và bảo toàn chúng khi chúng lan truyền ra môi trường hiện có như trên. Đặt exposeSessionEnvironment: false để tắt chúng:
const bashTool = createBashTool(cwd, {
exposeSessionEnvironment: false,
});Xem Bash tool session environment để biết ngữ nghĩa thay đổi. Xem examples/extensions/ssh.ts để biết ví dụ SSH hoàn chỉnh với cờ --ssh.
Cắt ngắn đầu ra
Các công cụ PHẢI cắt bớt đầu ra để tránh làm choáng ngợp ngữ cảnh LLM. Đầu ra lớn có thể gây ra:
- Lỗi tràn ngữ cảnh (lời nhắc quá dài)
- Lỗi nén
- Hiệu suất mô hình bị suy giảm
Giới hạn tích hợp là 50KB (~10k mã thông báo) và 2000 dòng, tùy theo điều kiện nào được nhấn trước. Sử dụng các tiện ích cắt ngắn đã xuất:
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 }] };
}Các điểm chính:
- Sử dụng
truncateHeadcho nội dung có phần mở đầu quan trọng (kết quả tìm kiếm, lượt đọc tệp) - Sử dụng
truncateTailcho nội dung mà phần cuối quan trọng (nhật ký, đầu ra lệnh) - Luôn thông báo cho LLM khi đầu ra bị cắt bớt và tìm phiên bản đầy đủ ở đâu
- Ghi lại giới hạn cắt ngắn trong mô tả công cụ của bạn
Xem examples/extensions/truncated-tool.ts để biết ví dụ hoàn chỉnh về cách gói rg (ripgrep) với cách cắt ngắn thích hợp.
Nhiều công cụ
Một tiện ích mở rộng có thể đăng ký nhiều công cụ với trạng thái chia sẻ:
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();
});
}Hiển thị tùy chỉnh
Các công cụ có thể cung cấp renderCall và renderResult để hiển thị TUI tùy chỉnh. Xem tui.md để biết thành phần đầy đủ API và tool-execution.ts để biết cách tạo các hàng công cụ.
Theo mặc định, đầu ra của công cụ được bao bọc trong Box xử lý phần đệm và nền. Một renderCall hoặc renderResult được xác định phải trả về Component. Nếu trình kết xuất vị trí không được xác định, tool-execution.ts sẽ sử dụng kết xuất dự phòng cho vị trí đó.
Đặt renderShell: "self" khi công cụ sẽ hiển thị shell của chính nó thay vì sử dụng Box mặc định. Điều này hữu ích cho các công cụ cần kiểm soát hoàn toàn hành vi đóng khung hoặc nền, ví dụ như các bản xem trước lớn phải ổn định về mặt hình ảnh sau khi công cụ ổn định.
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 và renderResult mỗi người nhận được một đối tượng context với:
args- đối số lệnh gọi công cụ hiện tạistate- trạng thái hàng cục bộ được chia sẻ trênrenderCallvàrenderResultlastComponent- thành phần được trả về trước đó cho vị trí đó, nếu cóinvalidate()- yêu cầu hiển thị lại hàng công cụ nàytoolCallId,cwd,executionStarted,argsComplete,isPartial,expanded,showImages,isError
Sử dụng context.state cho trạng thái chia sẻ giữa các khe. Giữ bộ đệm cục bộ theo vị trí trên phiên bản thành phần được trả về khi bạn muốn sử dụng lại và thay đổi cùng một thành phần trên các kết xuất.
kết xuấtCuộc gọi
Hiển thị lệnh gọi công cụ hoặc tiêu đề:
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;
}kết xuấtKết quả
Hiển thị kết quả hoặc đầu ra của công cụ:
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);
}Nếu một vị trí cố ý không có nội dung hiển thị, hãy trả về một Component trống, chẳng hạn như Container trống.
Gợi ý liên kết phím
Sử dụng keyHint() để hiển thị các gợi ý liên kết phím tuân theo cấu hình liên kết phím đang hoạt động:
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);
}Các chức năng có sẵn:
keyHint(keybinding, description)- Định dạng id liên kết phím được định cấu hình, chẳng hạn như"app.tools.expand"hoặc"tui.select.confirm"keyText(keybinding)- Trả về văn bản khóa được định cấu hình thô cho id liên kết phímrawKeyHint(key, description)- Định dạng chuỗi khóa thô
Sử dụng id liên kết phím được đặt tên:
- Id tác nhân mã hóa sử dụng không gian tên
app.*, ví dụ:app.tools.expand,app.editor.external,app.session.rename - Các id TUI được chia sẻ sử dụng không gian tên
tui.*, ví dụ:tui.select.confirm,tui.select.cancel,tui.input.tab
Để biết danh sách đầy đủ các id và mặc định liên kết phím, hãy xem keybindings.md. keybindings.json sử dụng các id được đặt tên giống nhau.
Trình chỉnh sửa tùy chỉnh và thành phần ctx.ui.custom() nhận keybindings: KeybindingsManager dưới dạng đối số được chèn. Họ nên sử dụng trực tiếp trình quản lý được chèn đó thay vì gọi getKeybindings() hoặc setKeybindings().
Thực tiễn tốt nhất
- Sử dụng
Textvới phần đệm(0, 0). Hộp mặc định xử lý phần đệm. - Sử dụng
\ncho nội dung nhiều dòng. - Xử lý
isPartialđể biết tiến trình phát trực tuyến. - Hỗ trợ
expandedđể biết chi tiết theo yêu cầu. - Giữ chế độ xem mặc định nhỏ gọn.
- Đọc
context.argstrongrenderResultthay vì sao chép đối số vàocontext.state. - Chỉ sử dụng
context.statecho dữ liệu phải được chia sẻ giữa các vùng cuộc gọi và kết quả. - Tái sử dụng
context.lastComponentkhi có thể cập nhật cùng một phiên bản thành phần. - Chỉ sử dụng
renderShell: "self"khi vỏ đóng hộp mặc định bị cản trở. Trong chế độ self-shell, công cụ này chịu trách nhiệm về khung, phần đệm và nền của chính nó.
Dự phòng
Nếu trình kết xuất vị trí không được xác định hoặc ném:
renderCall: Hiển thị tên công cụrenderResult: Hiển thị văn bản thô từcontent
Đang tải công cụ động
Extensions có thể đăng ký nhiều công cụ trong khi chỉ duy trì hoạt động một bộ công cụ ban đầu nhỏ. Sau đó, một công cụ có thể thêm nhiều công cụ hơn với pi.setActiveTools() trong khi thực thi. Pi phát hiện các thay đổi hoàn toàn bổ sung, ghi lại tên công cụ mới có sẵn trên kết quả công cụ đó và áp dụng bộ hoạt động được cập nhật trước yêu cầu mô hình tiếp theo.
Điều này hoạt động với mọi mô hình. Models với hỗ trợ tải hoãn lại gốc, duy trì tiền tố nhắc ổn định và tải các định nghĩa mới ở vị trí kết quả công cụ. Các mô hình khác sử dụng dự phòng được mô tả bên dưới.
Vòng đời là:
- Đăng ký mọi công cụ với
pi.registerTool()để nó xuất hiện trongpi.getAllTools(). - Giữ các công cụ của trình tải, chẳng hạn như
search_tools, ở trạng thái hoạt động và để các công cụ có thể tìm kiếm ở trạng thái không hoạt động. - Trong quá trình thực thi trình tải, hãy gọi
pi.setActiveTools([...currentTools,...matchingTools]). Thay đổi phải mang tính bổ sung: không xóa các công cụ hiện đang hoạt động trong cùng một lệnh gọi. - Pi ghi lại những công cụ nào đã được thêm vào kết quả công cụ của trình tải.
- Trước phản hồi mô hình tiếp theo, Pi hiển thị các định nghĩa đã thêm bằng cách sử dụng tải trì hoãn gốc khi được hỗ trợ hoặc danh sách công cụ hoạt động thông thường nếu không.
Bạn không cần phải trả lại các tham chiếu công cụ dành riêng cho nhà cung cấp hoặc đánh dấu trình tải là một công cụ tìm kiếm đặc biệt. Sự thay đổi công cụ hoạt động là tín hiệu. Tên được chuyển đến pi.setActiveTools() phải được đăng ký; những cái tên chưa biết sẽ bị bỏ qua.
Models với tải trả chậm gốc
- Nhân loại
- Models: Sonnet, Opus, Fable phiên bản 4.5 trở lên (không có Haiku)
- Biểu diễn gốc: Định nghĩa hoãn lại sử dụng
defer_loading; điểm tải sử dụng nội dungtool_reference.
- MởAI
- Models:
gpt-5.4và dòng mới hơn - Đại diện gốc: Pi thêm các mục khách hàng
tool_search_callvàtool_search_outputđã hoàn thành tại điểm tải.
- Models:
Đối với mô hình hoặc proxy tùy chỉnh đã được xác minh, bạn có thể bật tính năng xử lý gốc bằng compat.supportsToolReferences: true cho anthropic-messages hoặc compat.supportsToolSearch: true cho openai-responses và openai-codex-responses. Hãy tắt những tính năng này trừ khi điểm cuối và mô hình chấp nhận giao thức gốc tương ứng.
Hành vi dự phòng
Đối với tất cả các kiểu máy và nhà cung cấp khác, kích hoạt động vẫn hoạt động: Pi gửi danh sách công cụ hiện hoạt hoàn chỉnh một cách bình thường theo yêu cầu tiếp theo. Mô hình có thể gọi các công cụ mới được kích hoạt nhưng việc thêm định nghĩa của chúng có thể làm mất hiệu lực tiền tố lời nhắc được lưu trong bộ nhớ đệm của nhà cung cấp.
Pi cũng sử dụng dự phòng an toàn này khi bộ hoạt động không hoàn toàn là phụ gia, chẳng hạn như thay thế một nhóm công cụ này bằng một nhóm công cụ khác. Do đó, việc loại bỏ công cụ có tác dụng nhưng chúng không sử dụng chế độ tải chậm.
Để có hoạt động bộ nhớ đệm tốt nhất, hãy duy trì hoạt động của công cụ tải trong toàn bộ phiên và thêm các công cụ thay vì thay thế nhóm hoạt động. Cũng lưu ý rằng việc kích hoạt công cụ bằng promptSnippet hoặc promptGuidelines sẽ xây dựng lại lời nhắc hệ thống; thay đổi theo lời nhắc hệ thống đó có thể làm mất hiệu lực tiền tố ngay cả khi nhà cung cấp hỗ trợ các lược đồ trì hoãn. Các công cụ được tải chậm thường phải dựa vào công cụ description của chúng và bỏ qua siêu dữ liệu lời nhắc chỉ hoạt động.
Ví dụ về công cụ tìm kiếm
Tiện ích mở rộng sau đăng ký hai công cụ có thể tìm kiếm, xóa chúng khỏi nhóm hoạt động ban đầu và chỉ giữ search_tools làm trình tải của chúng. Ví dụ này sử dụng kết hợp từ khóa đơn giản nhưng việc triển khai tìm kiếm có thể sử dụng BM25, phần nhúng, danh mục từ xa hoặc định tuyến dành riêng cho dự án.
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"])]);
});
}Khi search_tools thêm một kết quả khớp, mô hình sẽ nhận được định nghĩa đó theo yêu cầu ngay sau đó. Trên mô hình có khả năng gốc, định nghĩa được neo sau kết quả tìm kiếm mà không thay đổi tiền tố lược đồ công cụ ban đầu. Trên các kiểu máy khác, nó xuất hiện trong danh sách công cụ thông thường theo cùng yêu cầu sau đây.
Giao diện người dùng tùy chỉnh
Extensions có thể tương tác với người dùng thông qua các phương thức ctx.ui và tùy chỉnh cách hiển thị thông báo/công cụ.
Đối với các thành phần tùy chỉnh, hãy xem tui.md có mẫu sao chép-dán cho:
- Hộp thoại lựa chọn (SelectList)
- Hoạt động không đồng bộ với hủy (BorderedLoader)
- Chuyển đổi cài đặt (Danh sách cài đặt)
- Chỉ báo trạng thái (setStatus)
- Thông báo, khả năng hiển thị và chỉ báo hoạt động trong khi phát trực tuyến (
setWorkingMessage,setWorkingVisible,setWorkingIndicator) - Các widget trên/dưới trình soạn thảo (setWidget)
- Trình cung cấp tự động hoàn thành được xếp chồng lên trên tính năng hoàn thành dấu gạch chéo/đường dẫn tích hợp sẵn (addAutocompleteProvider)
- Chân trang tùy chỉnh (setFooter)
Hộp thoại
// 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"Hộp thoại hẹn giờ với đếm ngược
Hộp thoại hỗ trợ tùy chọn timeout tự động loại bỏ bằng màn hình đếm ngược trực tiếp:
// 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
}Trả về giá trị khi hết thời gian chờ:
select()trả vềundefinedconfirm()trả vềfalseinput()trả vềundefined
Loại bỏ thủ công bằng AbortSignal
Để kiểm soát nhiều hơn (ví dụ: để phân biệt thời gian chờ với việc hủy của người dùng), hãy sử dụng 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")
}Xem examples/extensions/timed-confirm.ts để biết ví dụ đầy đủ.
Widget, trạng thái và chân trang
// 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 themeCác khung chỉ báo hoạt động tùy chỉnh được hiển thị nguyên văn. Nếu bạn muốn màu sắc, hãy tự thêm chúng vào chuỗi khung, ví dụ như bằng ctx.ui.theme.fg(...).
Tự động hoàn thành Providers
Sử dụng ctx.ui.addAutocompleteProvider() để xếp chồng logic tự động hoàn thành tùy chỉnh lên trên trình cung cấp đường dẫn và lệnh gạch chéo tích hợp sẵn. Đặt triggerCharacters cho trình kích hoạt tự nhiên tùy chỉnh, chẳng hạn như Sử dụng ctx.ui.addAutocompleteProvider()để xếp chồng logic tự động hoàn thành tùy chỉnh lên trên trình cung cấp đường dẫn và lệnh gạch chéo tích hợp sẵn. ĐặttriggerCharacters` cho trình kích hoạt tự nhiên tùy chỉnh, chẳng hạn như.
Mẫu điển hình:
- kiểm tra văn bản trước con trỏ
- trả lại đề xuất của riêng bạn khi cú pháp dành riêng cho tiện ích mở rộng của bạn khớp
- nếu không thì ủy quyền cho
current.getSuggestions(...) - ủy quyền
applyCompletion(...)trừ khi bạn cần hành vi chèn tùy chỉnh
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;
},
}));
});Xem github-issue-autocomplete.ts để biết ví dụ hoàn chỉnh tải trước các vấn đề GitHub mở mới nhất với gh issue list và lọc chúng cục bộ để hoàn thành #... nhanh chóng. Nó yêu cầu GitHub CLI (gh) và kiểm tra kho lưu trữ GitHub.
Thành phần tùy chỉnh
Đối với giao diện người dùng phức tạp, hãy sử dụng ctx.ui.custom(). Điều này tạm thời thay thế trình soạn thảo bằng thành phần của bạn cho đến khi done() được gọi:
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
}Cuộc gọi lại nhận được:
- Phiên bản
tui- TUI (đối với kích thước màn hình, quản lý tiêu điểm) theme- Chủ đề tạo kiểu hiện tạikeybindings- Trình quản lý liên kết phím ứng dụng (để kiểm tra phím tắt)done(value)- Lệnh gọi để đóng thành phần và trả về giá trị
Xem tui.md để biết thành phần đầy đủ API.
Chế độ lớp phủ (Thử nghiệm)
Vượt qua { overlay: true } để hiển thị thành phần dưới dạng phương thức nổi bên trên nội dung hiện có mà không xóa màn hình:
const result = await ctx.ui.custom<string | null>(
(tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
{ overlay: true }
);Để định vị nâng cao (neo, lề, tỷ lệ phần trăm, khả năng hiển thị phản hồi), hãy chuyển overlayOptions. Sử dụng onHandle để kiểm soát tiêu điểm hoặc khả năng hiển thị theo chương trình:
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
}
}
);Lớp phủ hiển thị tập trung có thể lấy lại thông tin đầu vào sau khi giao diện người dùng tùy chỉnh không có lớp phủ tạm thời đóng lại. Nếu bạn cố tình muốn một thành phần khác giữ nguyên dữ liệu đầu vào trong khi lớp phủ vẫn hiển thị, hãy gọi handle.unfocus({ target }). Việc chuyển { target: null } sẽ giải phóng lớp phủ mà không tập trung vào thành phần khác.
Xem tui.md để biết đầy đủ OverlayOptions và OverlayHandle API và overlay-qa-tests.ts để biết ví dụ.
Trình chỉnh sửa tùy chỉnh
Thay thế trình chỉnh sửa đầu vào chính bằng cách triển khai tùy chỉnh (chế độ vim, chế độ emacs, v.v.):
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)
);
});
}Các điểm chính:
- Mở rộng
CustomEditor(không phải cơ sởEditor) để nhận các tổ hợp phím ứng dụng (thoát để hủy bỏ, ctrl+d, chuyển đổi mô hình) - Gọi
super.handleInput(data)để biết các phím bạn không xử lý - Nhà máy nhận được
tui,themevàkeybindingstừ ứng dụng - Sử dụng
ctx.ui.getEditorComponent()trướcsetEditorComponent()để gói trình chỉnh sửa tùy chỉnh đã định cấu hình trước đó - Vượt qua
undefinedđể khôi phục mặc định:ctx.ui.setEditorComponent(undefined)
Để soạn thảo bằng một tiện ích mở rộng khác đã thay thế trình chỉnh sửa, hãy nắm bắt nhà máy trước đó trước khi cài đặt tiện ích mở rộng của bạn:
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);Xem tui.md Mẫu 7 để biết ví dụ hoàn chỉnh về chỉ báo chế độ.
Hiển thị tin nhắn và mục nhập
Đăng ký trình kết xuất tùy chỉnh cho tin nhắn bằng customType của bạn. Sử dụng trình kết xuất thông báo cho nội dung cần tham gia vào ngữ cảnh 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);
});Tin nhắn được gửi qua pi.sendMessage():
pi.sendMessage({
customType: "my-extension", // Matches registerMessageRenderer
content: "Status update",
display: true, // Show in TUI
details: { ... }, // Available in renderer
});Đối với nội dung chỉ dành cho TUI không được gửi tới LLM, thay vào đó hãy hiển thị các mục tùy chỉnh:
pi.registerEntryRenderer("my-card", (entry, options, theme) => {
return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});
pi.appendEntry("my-card", { status: "done" });Màu chủ đề
Tất cả các hàm kết xuất đều nhận được đối tượng theme. Xem themes.md để tạo chủ đề tùy chỉnh và bảng màu đầy đủ.
// 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)Để đánh dấu cú pháp trong trình kết xuất công cụ tùy chỉnh:
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);Xử lý lỗi
- Lỗi tiện ích mở rộng được ghi lại, tác nhân tiếp tục
tool_calllỗi chặn công cụ (không an toàn)- Lỗi công cụ
executephải được báo hiệu bằng cách ném; lỗi ném được phát hiện, được báo cáo cho LLM vớiisError: truevà quá trình thực thi tiếp tục
Chế độ hành vi
| Cách thức | ctx.mode |
ctx.hasUI |
Ghi chú |
|---|---|---|---|
| Tương tác | "tui" |
true |
Đầy đủ TUI với kết xuất thiết bị đầu cuối |
RPC (--mode rpc) |
"rpc" |
true |
Hộp thoại và thông báo qua giao thức JSON; custom() trả về undefined. Xem rpc.md |
JSON (--mode json) |
"json" |
false |
Luồng sự kiện tới stdout; Các phương thức giao diện người dùng không hoạt động |
In (-p) |
"print" |
false |
Extensions chạy nhưng không nhắc được |
Sử dụng ctx.mode === "tui" trước các tính năng dành riêng cho TUI (custom(), nhà máy sản xuất thành phần, đầu vào thiết bị đầu cuối). Sử dụng ctx.hasUI trước các phương thức hộp thoại và thông báo hoạt động ở cả hai chế độ TUI và RPC.
Ví dụ tham khảo
Tất cả các ví dụ trong examples/extensions/.
| Ví dụ | Sự miêu tả | Chìa khóa APIs |
|---|---|---|
| Công cụ | ||
hello.ts |
Đăng ký công cụ tối thiểu | registerTool |
question.ts |
Công cụ có sự tương tác của người dùng | registerTool, ui.select |
questionnaire.ts |
Công cụ hướng dẫn nhiều bước | registerTool, ui.custom |
todo.ts |
Công cụ trạng thái có tính bền bỉ | registerTool, appendEntry, renderResult, sự kiện phiên |
dynamic-tools.ts |
Đăng ký công cụ sau khi khởi động và trong khi ra lệnh | registerTool, session_start, registerCommand |
structured-output.ts |
Công cụ đầu ra có cấu trúc cuối cùng với terminate: true |
registerTool, kết quả công cụ chấm dứt |
truncated-tool.ts |
Ví dụ cắt ngắn đầu ra | registerTool, truncateHead |
tool-override.ts |
Ghi đè công cụ đọc tích hợp | registerTool (cùng tên với phần tích hợp sẵn) |
| Lệnh | ||
pirate.ts |
Sửa đổi lời nhắc hệ thống mỗi lượt | registerCommand, before_agent_start |
summarize.ts |
Lệnh tóm tắt cuộc hội thoại | registerCommand, ui.custom |
handoff.ts |
Chuyển giao mô hình giữa các nhà cung cấp | registerCommand, ui.editor, ui.custom |
qna.ts |
Hỏi đáp với giao diện người dùng tùy chỉnh | registerCommand, ui.custom, setEditorText |
send-user-message.ts |
Chèn tin nhắn của người dùng | registerCommand, sendUserMessage |
reload-runtime.ts |
Lệnh tải lại và chuyển giao công cụ LLM | registerCommand, ctx.reload(), sendUserMessage |
shutdown-command.ts |
Lệnh tắt duyên dáng | registerCommand, shutdown() |
| Sự kiện & Cổng | ||
permission-gate.ts |
Chặn các lệnh nguy hiểm | on("tool_call"), ui.confirm |
project-trust.ts |
Quyết định hoặc trì hoãn sự tin cậy của dự án từ tiện ích mở rộng người dùng/toàn cầu hoặc CLI | on("project_trust"), giao diện người dùng tin cậy, kết quả tin cậy bắt buộc |
protected-paths.ts |
Chặn ghi vào các đường dẫn cụ thể | on("tool_call") |
confirm-destructive.ts |
Xác nhận thay đổi phiên | on("session_before_switch"), on("session_before_fork") |
dirty-repo-guard.ts |
Cảnh báo về repo git bẩn | on("session_before_*"), exec |
input-transform.ts |
Chuyển đổi đầu vào của người dùng | on("input") |
input-transform-streaming.ts |
Chuyển đổi đầu vào nhận biết luồng | on("input"), streamingBehavior |
model-status.ts |
React để thay đổi mô hình | on("model_select"), setStatus |
provider-payload.ts |
Kiểm tra tải trọng và tiêu đề phản hồi của nhà cung cấp | on("before_provider_request"), on("after_provider_response") |
system-prompt-header.ts |
Hiển thị thông tin nhắc nhở hệ thống | on("agent_start"), getSystemPrompt |
claude-rules.ts |
Tải quy tắc từ tập tin | on("session_start"), on("before_agent_start") |
prompt-customizer.ts |
Thêm hướng dẫn về công cụ nhận biết ngữ cảnh bằng cách sử dụng systemPromptOptions |
on("before_agent_start"), BuildSystemPromptOptions |
file-trigger.ts |
Trình xem tập tin kích hoạt tin nhắn | sendMessage |
| Nén & Phiên | ||
custom-compaction.ts |
Tóm tắt nén tùy chỉnh | on("session_before_compact") |
trigger-compact.ts |
Kích hoạt nén thủ công | compact() |
git-checkpoint.ts |
Git cất theo lượt | on("turn_start"), on("session_before_fork"), exec |
git-merge-and-resolve.ts |
Tìm nạp, hợp nhất và giải quyết xung đột | on("agent_end"), exec, sendUserMessage |
auto-commit-on-exit.ts |
Cam kết tắt máy | on("session_shutdown"), exec |
| Thành phần giao diện người dùng | ||
status-line.ts |
Chỉ báo trạng thái chân trang | setStatus, sự kiện phiên |
working-indicator.ts |
Tùy chỉnh chỉ báo hoạt động phát trực tuyến | setWorkingIndicator, registerCommand |
github-issue-autocomplete.ts |
Thêm #1234 số lần hoàn thành vấn đề bên cạnh tính năng tự động hoàn thành được tích hợp sẵn bằng cách tải trước các vấn đề đang mở gần đây từ gh issue list |
addAutocompleteProvider, on("session_start"), exec |
custom-footer.ts |
Thay thế hoàn toàn chân trang | registerCommand, setFooter |
custom-header.ts |
Thay thế tiêu đề khởi động | on("session_start"), setHeader |
modal-editor.ts |
Trình chỉnh sửa phương thức kiểu Vim | setEditorComponent, CustomEditor |
rainbow-editor.ts |
Kiểu trình chỉnh sửa tùy chỉnh | setEditorComponent |
widget-placement.ts |
Widget trên/dưới trình chỉnh sửa | setWidget |
overlay-test.ts |
Thành phần lớp phủ | ui.custom với các tùy chọn lớp phủ |
overlay-qa-tests.ts |
Kiểm tra lớp phủ toàn diện | ui.custom, tất cả các tùy chọn lớp phủ |
notify.ts |
Thông báo đơn giản | ui.notify |
timed-confirm.ts |
Hộp thoại có thời gian chờ | ui.confirm với thời gian chờ/tín hiệu |
mac-system-theme.ts |
Tự động chuyển đổi chủ đề | setTheme, exec |
| Phức tạp Extensions | ||
plan-mode/ |
Thực hiện chế độ kế hoạch đầy đủ | Tất cả các loại sự kiện, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools |
preset.ts |
Các cài đặt trước có thể lưu được (mô hình, công cụ, tư duy) | registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry |
tools.ts |
Bật/tắt công cụ UI | registerCommand, setActiveTools, SettingsList, sự kiện phiên |
| Điều khiển từ xa & Hộp cát | ||
ssh.ts |
SSH thực thi từ xa | registerFlag, on("user_bash"), on("before_agent_start"), thao tác với công cụ |
interactive-shell.ts |
Phiên shell liên tục | on("user_bash") |
sandbox/ |
Thực thi công cụ hộp cát | Hoạt động của công cụ |
gondolin/ |
Định tuyến các công cụ tích hợp và lệnh ! vào micro-VM Gondolin |
Thao tác với công cụ, ghi đè công cụ tích hợp, on("user_bash") |
subagent/ |
Đại lý phụ sinh sản | registerTool, exec |
| Trò chơi | ||
snake.ts |
Trò chơi rắn | registerCommand, ui.custom, xử lý bàn phím |
space-invaders.ts |
Trò chơi Kẻ xâm lược không gian | registerCommand, ui.custom |
doom-overlay/ |
Sự diệt vong trong lớp phủ | ui.custom với lớp phủ |
| Providers | ||
custom-provider-anthropic/ |
Proxy nhân loại tùy chỉnh | registerProvider |
custom-provider-gitlab-duo/ |
Git Tích hợp Lab Duo | registerProvider với OAuth |
| Tin nhắn & Giao tiếp | ||
message-renderer.ts |
Hiển thị tin nhắn tùy chỉnh | registerMessageRenderer, sendMessage |
entry-renderer.ts |
Kết xuất mục nhập tùy chỉnh chỉ TUI | registerEntryRenderer, appendEntry |
event-bus.ts |
Sự kiện liên mở rộng | pi.events |
| Siêu dữ liệu phiên | ||
session-name.ts |
Đặt tên phiên cho bộ chọn | setSessionName, getSessionName |
bookmark.ts |
Đánh dấu các mục cho /cây | setLabel |
| Khác | ||
inline-bash.ts |
Nội tuyến bash trong lệnh gọi công cụ | on("tool_call") |
bash-spawn-hook.ts |
Điều chỉnh lệnh bash, cwd và env trước khi thực thi | createBashTool, spawnHook |
with-deps/ |
Tiện ích mở rộng với phần phụ thuộc npm | Cấu trúc gói với package.json |