Cấu hình, tùy chỉnh, thiết lập nền tảng và tham chiếu API cho Pi.

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ụng pi -e./path.ts cho 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ư /mycommand qua pi.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

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.ts

Vị 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.ts

Thư 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 module

Gó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_shutdown

Sự 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"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"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.systemPromptctx.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_startmessage_end kích hoạt thông báo kết quả của người dùng, trợ lý và công cụ.
  • message_update kí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_end có thể trả về { message } để thay thế thông báo cuối cùng. Việc thay thế phải giữ nguyên role.
pi.on("message_start", async (event, ctx) => {
  // event.message
});

pi.on("message_update", async (event, ctx) => {
  // event.message
  // event.assistantMessageEvent (token-by-token stream event)
});

pi.on("message_end", async (event, ctx) => {
  if (event.message.role !== "assistant") return;

  return {
    message: {
      ...event.message,
      usage: {
        ...event.message.usage,
        cost: {
          ...event.message.usage.cost,
          total: 0.123,
        },
      },
    },
  };
});

tool_execution_start / tool_execution_update / tool_execution_end

Đượ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áng
  • tool_execution_update sự 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 toolResult cuố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_call sau 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_call thông qua { block: true, reason?: string, terminate?: boolean }
  • terminate chỉ á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_resulttool_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, isError hoặc usage); 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/template chưa được mở rộng.

Xử lý đơn hàng:

  1. 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
  2. input cháy sự kiện - có thể chặn, biến đổi hoặc xử lý
  3. 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
  4. Nếu không được xử lý: prompt templates (/template) được mở rộng sang nội dung mẫu
  5. 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ộng
  • handled - 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.tsinput-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 ID

ctx.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_updateturn_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 onCompleteonError 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 context sau 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ới
  • setup: thay đổi SessionManager của phiên mới trước khi withSession chạy
  • 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ụng pi / lệnh ctx cũ đã 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ửa
  • position: "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ảo
  • 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ụng pi / lệnh ctx cũ đã 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ông
  • customInstructions: Hướng dẫn tùy chỉnh cho phần tóm tắt
  • replaceInstructions: Nếu đúng, customInstructions thay thế lời nhắc mặc định thay vì được thêm vào
  • label: 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:

Để 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()sendUserMessage() được liên kết với phiên thay thế.

Vòng đời và súng ngắn:

  • withSession chỉ chạy sau khi phiên cũ đã phát ra session_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 được session_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 withSession bắ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ụng ctx được chuyển đến withSession cho 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.sessionManager trước khi thay thế, sm vẫn là đối tượng SessionManager cũ. Không sử dụng lại nó sau khi thay thế.
  • Mã trong withSession sẽ giả sử bất kỳ trạng thái nào bị trình xử lý session_shutdown củ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 ra session_shutdown cho 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_start với reason: "reload"resources_discover vớ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 toolspromptGuidelines để 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""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/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/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"
  • isStreamingtrue để cập nhật một phần trợ lý; false dành cho người dùng, trợ lý đã hoàn thiện và tin nhắn được khôi phục
  • availableWidth — 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.killed

pi.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-only

pi.getAllTools() trả về name, description, parameters, promptGuidelinessourceInfo.

Giá trị sourceInfo.source điển hình:

  • builtin cho các công cụ tích hợp
  • sdk đối với các công cụ được chuyển qua createAgentSession({ 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()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, streamstreamSimple.

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_VAR hoặ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ấp oauth). $ 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ấp oauth). $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ể đặt baseUrl để 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.stored chứa ảnh chụp nhanh của nhà cung cấp liên tục; chỉ sử dụng context.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ụng persist: 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ư editwrite 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 oldTextnewText 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.ts

Ngoà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.ts

Xem 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ở: promptSnippetpromptGuidelines 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:

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_MODELPI_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 truncateHead cho 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 truncateTail cho 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 renderCallrenderResult để 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);
  },
});

renderCallrenderResult 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ại
  • state - trạng thái hàng cục bộ được chia sẻ trên renderCallrenderResult
  • lastComponent - 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ày
  • toolCallId, 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ím
  • rawKeyHint(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 Text với phần đệm (0, 0). Hộp mặc định xử lý phần đệm.
  • Sử dụng \n cho 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.args trong renderResult thay vì sao chép đối số vào context.state.
  • Chỉ sử dụng context.state cho 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.lastComponent khi 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à:

  1. Đăng ký mọi công cụ với pi.registerTool() để nó xuất hiện trong pi.getAllTools().
  2. 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.
  3. 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.
  4. 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.
  5. 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 dung tool_reference.
  • MởAI
    • Models: gpt-5.4 và dòng mới hơn
    • Đại diện gốc: Pi thêm các mục khách hàng tool_search_calltool_search_output đã hoàn thành tại điểm tải.

Đố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-responsesopenai-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ề undefined
  • confirm() trả về false
  • input() 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 theme

Cá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ại
  • keybindings - 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 đủ OverlayOptionsOverlayHandle 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, themekeybindings từ ứng dụng
  • Sử dụng ctx.ui.getEditorComponent() trước setEditorComponent() để 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_call lỗi chặn công cụ (không an toàn)
  • Lỗi công cụ execute phả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ới isError: true và 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