Konfigurasi, kustomisasi, pengaturan platform, dan referensi API untuk Pi.

Extensions

pi dapat membuat ekstensi. Mintalah untuk membuat satu untuk kasus penggunaan Anda.

Extensions adalah modul TypeScript yang memperluas perilaku pi. Mereka dapat berlangganan peristiwa siklus hidup, mendaftarkan alat khusus yang dapat dipanggil oleh LLM, menambahkan perintah, dan banyak lagi.

Penempatan untuk /muat ulang: Masukkan ekstensi di ~/.pi/agent/extensions/ (global) atau .pi/extensions/ (proyek-lokal) untuk penemuan otomatis. Gunakan pi -e./path.ts hanya untuk tes cepat. Extensions di lokasi yang ditemukan secara otomatis dapat diisi ulang dengan /reload.

Kemampuan utama:

  • Alat khusus - Daftarkan alat yang dapat dihubungi LLM melalui pi.registerTool()
  • Intersepsi peristiwa - Memblokir atau mengubah panggilan alat, memasukkan konteks, menyesuaikan pemadatan
  • Interaksi pengguna - Meminta pengguna melalui ctx.ui (pilih, konfirmasi, masukkan, beri tahu)
  • Komponen UI khusus - Komponen TUI lengkap dengan input keyboard melalui ctx.ui.custom() untuk interaksi kompleks
  • Perintah khusus - Daftarkan perintah seperti /mycommand melalui pi.registerCommand()
  • Kegigihan sesi - Status penyimpanan yang bertahan saat dimulai ulang melalui pi.appendEntry()
  • Render khusus - Kontrol bagaimana panggilan alat/hasil dan pesan muncul di TUI

Contoh kasus penggunaan:

  • Gerbang izin (konfirmasi sebelum rm -rf, sudo, dll.)
  • Git pos pemeriksaan (simpanan di setiap belokan, pulihkan di cabang)
  • Perlindungan jalur (blok tulis ke .env, node_modules/)
  • Pemadatan khusus (ringkas percakapan sesuai keinginan Anda)
  • Ringkasan percakapan (lihat contoh summarize.ts)
  • Alat interaktif (pertanyaan, penyihir, dialog khusus)
  • Alat stateful (daftar tugas, kumpulan koneksi)
  • Integrasi eksternal (pengamat file, webhook, pemicu CI)
  • Permainan sambil menunggu (lihat contoh snake.ts)

Lihat examples/extensions/ untuk implementasi kerja.

Daftar isi

Mulai Cepat

Buat ~/.pi/agent/extensions/my-extension.ts:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  // React to events
  pi.on("session_start", async (_event, ctx) => {
    ctx.ui.notify("Extension loaded!", "info");
  });

  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
      if (!ok) return { block: true, reason: "Blocked by user" };
    }
  });

  // Register a custom tool
  pi.registerTool({
    name: "greet",
    label: "Greet",
    description: "Greet someone by name",
    parameters: Type.Object({
      name: Type.String({ description: "Name to greet" }),
    }),
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      return {
        content: [{ type: "text", text: `Hello, ${params.name}!` }],
        details: {},
      };
    },
  });

  // Register a command
  pi.registerCommand("hello", {
    description: "Say hello",
    handler: async (args, ctx) => {
      ctx.ui.notify(`Hello ${args || "world"}!`, "info");
    },
  });
}

Uji dengan tanda --extension (atau -e):

pi -e ./my-extension.ts

Lokasi Perluasan

Keamanan: Extensions dijalankan dengan izin sistem penuh Anda dan dapat mengeksekusi kode arbitrer. Instal hanya dari sumber yang Anda percayai.

Extensions ditemukan secara otomatis dari lokasi tepercaya. Entri proyek-lokal .pi/extensions dimuat hanya setelah proyek dipercaya.

Lokasi Cakupan
~/.pi/agent/extensions/*.ts Global (semua proyek)
~/.pi/agent/extensions/*/index.ts Global (subdirektori)
.pi/extensions/*.ts Proyek-lokal
.pi/extensions/*/index.ts Proyek-lokal (subdirektori)

Jalur tambahan melalui settings.json:

{
  "packages": [
    "npm:@foo/bar@1.0.0",
    "git:github.com/user/repo@v1"
  ],
  "extensions": [
    "/path/to/local/extension.ts",
    "/path/to/local/extension/dir"
  ]
}

Untuk berbagi ekstensi melalui npm atau paket git sebagai pi, lihat packages.md.

Impor yang Tersedia

Kemasan Tujuan
@earendil-works/pi-coding-agent Jenis ekstensi (ExtensionAPI, ExtensionContext, acara)
typebox Definisi skema untuk parameter alat
@earendil-works/pi-ai Utilitas AI (StringEnum untuk enum yang kompatibel dengan Google)
@earendil-works/pi-tui TUI komponen untuk rendering khusus

npm dependensi juga berfungsi. Tambahkan package.json di sebelah ekstensi Anda (atau di direktori induk), jalankan npm install, dan impor dari node_modules/ diselesaikan secara otomatis.

Untuk paket pi terdistribusi yang diinstal dengan pi install (npm atau git), deps runtime harus dalam dependencies. Instalasi paket menggunakan instalasi produksi (npm install --omit=dev) secara default, jadi devDependencies tidak tersedia saat runtime; ketika npmCommand dikonfigurasi, paket git menggunakan install biasa untuk kompatibilitas dengan pembungkus.

Node.js bawaan (node:fs, node:path, dll.) juga tersedia.

Ekstensi mengekspor fungsi default pabrik yang menerima ExtensionAPI. Pabrik bisa sinkron atau asinkron:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // Subscribe to events
  pi.on("event_name", async (event, ctx) => {
    // ctx.ui for user interaction
    const ok = await ctx.ui.confirm("Title", "Are you sure?");
    ctx.ui.notify("Done!", "info");
    ctx.ui.setStatus("my-ext", "Processing...");  // Footer status
    ctx.ui.setWidget("my-ext", ["Line 1", "Line 2"]);  // Widget above editor (default)
  });

  // Register tools, commands, shortcuts, flags
  pi.registerTool({ ... });
  pi.registerCommand("name", { ... });
  pi.registerShortcut("ctrl+x", { ... });
  pi.registerFlag("my-flag", { ... });
}

Extensions dimuat melalui jiti, jadi TypeScript berfungsi tanpa kompilasi.

Jika pabrik mengembalikan Promise, pi menunggunya sebelum melanjutkan startup. Itu berarti inisialisasi asinkron selesai sebelum session_start, sebelum resources_discover, dan sebelum pendaftaran penyedia yang diantri melalui pi.registerProvider() dihapus.

Fungsi pabrik asinkron

Gunakan pabrik async untuk pekerjaan startup satu kali seperti mengambil konfigurasi jarak jauh atau menemukan model yang tersedia secara dinamis.

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default async function (pi: ExtensionAPI) {
  const response = await fetch("http://localhost:1234/v1/models");
  const payload = (await response.json()) as {
    data: Array<{
      id: string;
      name?: string;
      context_window?: number;
      max_tokens?: number;
    }>;
  };

  pi.registerProvider("local-openai", {
    baseUrl: "http://localhost:1234/v1",
    apiKey: "$LOCAL_OPENAI_API_KEY",
    api: "openai-completions",
    models: payload.data.map((model) => ({
      id: model.id,
      name: model.name ?? model.id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: model.context_window ?? 128000,
      maxTokens: model.max_tokens ?? 4096,
    })),
  });
}

Pola ini membuat model yang diambil tersedia selama pengaktifan normal dan hingga pi --list-models.

Sumber daya berumur panjang dan penutupan

Pabrik ekstensi dapat berjalan dalam pemanggilan yang tidak pernah memulai sesi. Jangan memulai sumber daya latar belakang seperti proses, soket, pengamat file, atau pengatur waktu dari pabrik.

Tunda pengaktifan sumber daya latar belakang hingga session_start atau perintah/alat/peristiwa yang memerlukan sumber daya. Daftarkan penangan session_shutdown idempoten untuk menutup sumber daya cakupan sesi apa pun yang Anda mulai.

Gaya Ekstensi

File tunggal - paling sederhana, untuk ekstensi kecil:

~/.pi/agent/extensions/
└── my-extension.ts

Direktori dengan index.ts - untuk ekstensi multi-file:

~/.pi/agent/extensions/
└── my-extension/
    ├── index.ts        # Entry point (exports default function)
    ├── tools.ts        # Helper module
    └── utils.ts        # Helper module

Paket dengan dependensi - untuk ekstensi yang memerlukan npm paket:

~/.pi/agent/extensions/
└── my-extension/
    ├── package.json    # Declares dependencies and entry points
    ├── package-lock.json
    ├── node_modules/   # After npm install
    └── src/
        └── index.ts
// package.json
{
  "name": "my-extension",
  "dependencies": {
    "zod": "^3.0.0",
    "chalk": "^5.0.0"
  },
  "pi": {
    "extensions": ["./src/index.ts"]
  }
}

Jalankan npm install di direktori ekstensi, lalu impor dari node_modules/ berfungsi secara otomatis.

Acara

Ikhtisar Siklus Hidup

pi starts
  │
  ├─► project_trust (user/global and CLI extensions only, before project resources load)
  ├─► session_start { reason: "startup" }
  └─► resources_discover { reason: "startup" }
      │
      ▼
user sends prompt ─────────────────────────────────────────┐
  │                                                        │
  ├─► (extension commands checked first, bypass if found)  │
  ├─► input (can intercept, transform, or handle)          │
  ├─► (skill/template expansion if not handled)            │
  ├─► before_agent_start (can inject message, modify system prompt)
  ├─► agent_start                                          │
  ├─► message_start / message_update / message_end         │
  │                                                        │
  │   ┌─── turn (repeats while LLM calls tools) ───┐       │
  │   │                                            │       │
  │   ├─► turn_start                               │       │
  │   ├─► context (can modify messages)            │       │
  │   ├─► before_provider_headers (can mutate headers)     |
  │   ├─► before_provider_request (can inspect or replace payload)
  │   ├─► after_provider_response (status + headers, before stream consume)
  │   │                                            │       │
  │   │   LLM responds, may call tools:            │       │
  │   │     ├─► tool_execution_start               │       │
  │   │     ├─► tool_call (can block)              │       │
  │   │     ├─► tool_execution_update              │       │
  │   │     ├─► tool_result (can modify)           │       │
  │   │     └─► tool_execution_end                 │       │
  │   │                                            │       │
  │   └─► turn_end                                 │       │
  │                                                        │
  ├─► agent_end                                            │
  └─► agent_settled (no retry/compaction/follow-up left)   │
                                                           │
user sends another prompt ◄────────────────────────────────┘

/new (new session) or /resume (switch session)
  ├─► session_before_switch (can cancel)
  ├─► session_shutdown
  ├─► session_start { reason: "new" | "resume", previousSessionFile? }
  └─► resources_discover { reason: "startup" }

/fork or /clone
  ├─► session_before_fork (can cancel)
  ├─► session_shutdown
  ├─► session_start { reason: "fork", previousSessionFile }
  └─► resources_discover { reason: "startup" }

/name or pi.setSessionName()
  └─► session_info_changed

/compact or auto-compaction
  ├─► session_before_compact (can cancel or customize)
  └─► session_compact

/tree navigation
  ├─► session_before_tree (can cancel or customize)
  └─► session_tree

/model or Ctrl+P (model selection/cycling)
  ├─► thinking_level_select (if model change changes/clamps thinking level)
  └─► model_select

thinking level changes (settings, keybinding, pi.setThinkingLevel())
  └─► thinking_level_select

exit (Ctrl+C, Ctrl+D, SIGHUP, SIGTERM)
  └─► session_shutdown

Acara Permulaan

proyek_kepercayaan

Diaktifkan sebelum pi memutuskan apakah akan mempercayai proyek dengan konfigurasi dinamis (.pi atau .agents/skills). Ini berjalan saat startup dan ketika penggantian sesi (misalnya /resume) memasuki cwd yang kepercayaannya belum terselesaikan dalam proses saat ini. Hanya ekstensi pengguna/global dan ekstensi CLI -e yang berpartisipasi; ekstensi proyek-lokal tidak dimuat sampai kepercayaan teratasi.

pi.on("project_trust", async (event, ctx) => {
  // event.cwd - current working directory
  // ctx has a limited trust context: cwd, mode, hasUI, and select/confirm/input/notify UI helpers
  if (await ctx.ui.confirm("Trust project?", event.cwd)) {
    return { trusted: "yes", remember: true };
  }
  return { trusted: "undecided" };
});

Penangan project_trust harus mengembalikan { trusted: "yes" | "no" | "undecided" }. Ekstensi pengguna/global atau CLI yang mengembalikan "yes" atau "no" memiliki keputusan; keputusan ya/tidak yang pertama menang dan menekan perintah kepercayaan yang ada di dalamnya. Gunakan remember: true untuk mempertahankan keputusan ya/tidak; jika tidak, ini hanya berlaku untuk proses saat ini. Kembalikan "undecided" agar penangan selanjutnya atau aliran kepercayaan bawaan dapat memutuskan. Periksa ctx.hasUI sebelum meminta. Jika tidak ada pengendali yang mengembalikan ya/tidak, resolusi kepercayaan normal berlanjut: keputusan trust.json yang disimpan diterapkan terlebih dahulu, lalu defaultProjectTrust mengontrol apakah pi bertanya, memercayai, atau menolak secara default.

Peristiwa Sumber Daya

sumber daya_temukan

Diaktifkan setelah session_start sehingga ekstensi dapat menyumbangkan keahlian tambahan, prompt, dan jalur tema. Jalur startup menggunakan reason: "startup". Muat ulang menggunakan reason: "reload".

pi.on("resources_discover", async (event, _ctx) => {
  // event.cwd - current working directory
  // event.reason - "startup" | "reload"
  return {
    skillPaths: ["/path/to/skills"],
    promptPaths: ["/path/to/prompts"],
    themePaths: ["/path/to/themes"],
  };
});

Acara Sesi

Lihat Session Format untuk penyimpanan sesi internal dan SessionManager API.

sesi_mulai

Diaktifkan saat sesi dimulai, dimuat, atau dimuat ulang.

pi.on("session_start", async (event, ctx) => {
  // event.reason - "startup" | "reload" | "new" | "resume" | "fork"
  // event.previousSessionFile - present for "new", "resume", and "fork"
  ctx.ui.notify(`Session: ${ctx.sessionManager.getSessionFile() ?? "ephemeral"}`, "info");
});

sesi_info_berubah

Diaktifkan ketika nama tampilan sesi saat ini diatur melalui /name, RPC, atau pi.setSessionName().

pi.on("session_info_changed", async (event, ctx) => {
  // event.name - current normalized name, or undefined if cleared
  ctx.ui.notify(`Session renamed: ${event.name ?? "(none)"}`, "info");
});

sesi_sebelum_beralih

Diaktifkan sebelum memulai sesi baru (/new) atau berpindah sesi (/resume).

pi.on("session_before_switch", async (event, ctx) => {
  // event.reason - "new" or "resume"
  // event.targetSessionFile - session we're switching to (only for "resume")

  if (event.reason === "new") {
    const ok = await ctx.ui.confirm("Clear?", "Delete all messages?");
    if (!ok) return { cancel: true };
  }
});

Setelah tindakan peralihan atau sesi baru berhasil, pi mengeluarkan session_shutdown untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan session_start dengan reason: "new" | "resume" dan previousSessionFile. Lakukan pekerjaan pembersihan di session_shutdown, lalu bangun kembali status dalam memori di session_start.

sesi_sebelum_fork

Dipecat saat melakukan forking melalui /fork atau mengkloning melalui /clone.

pi.on("session_before_fork", async (event, ctx) => {
  // event.entryId - ID of the selected entry
  // event.position - "before" for /fork, "at" for /clone
  return { cancel: true }; // Cancel fork/clone
  // OR
  return { skipConversationRestore: true }; // Reserved for future conversation restore control
});

Setelah fork atau kloning berhasil, pi mengeluarkan session_shutdown untuk instance ekstensi lama, memuat ulang dan mengikat ulang ekstensi untuk sesi baru, lalu memancarkan session_start dengan reason: "fork" dan previousSessionFile. Lakukan pekerjaan pembersihan di session_shutdown, lalu bangun kembali status dalam memori di session_start.

session_before_compact / session_compact

Ditembak saat pemadatan. Lihat compaction.md untuk detailnya.

pi.on("session_before_compact", async (event, ctx) => {
  const { preparation, branchEntries, customInstructions, reason, willRetry, signal } = event;

  // reason - "manual" (/compact), "threshold", or "overflow"
  // willRetry - whether the aborted turn is retried after compaction (overflow recovery)

  // Cancel:
  return { cancel: true };

  // Custom summary:
  return {
    compaction: {
      summary: "...",
      firstKeptEntryId: preparation.firstKeptEntryId,
      tokensBefore: preparation.tokensBefore,
      // usage: summaryResponse.usage, // Optional; included in session totals
    }
  };
});

pi.on("session_compact", async (event, ctx) => {
  // event.compactionEntry - the saved compaction
  // event.fromExtension - whether extension provided it
  // event.reason - "manual" (/compact), "threshold", or "overflow"
  // event.willRetry - whether the aborted turn is retried after compaction (overflow recovery)
});

session_before_tree / session_tree

Ditembak pada navigasi /tree. Lihat Sessions untuk konsep navigasi pohon.

pi.on("session_before_tree", async (event, ctx) => {
  const { preparation, signal } = event;
  return { cancel: true };
  // OR provide custom summary:
  return {
    summary: {
      summary: "...",
      // usage: summaryResponse.usage, // Optional; included in session totals
      details: {},
    },
  };
});

pi.on("session_tree", async (event, ctx) => {
  // event.newLeafId, oldLeafId, summaryEntry, fromExtension
});

sesi_shutdown

Diaktifkan sebelum runtime sesi yang dimulai dirobohkan. Gunakan ini untuk membersihkan sumber daya yang dibuka dari session_start atau kait cakupan sesi lainnya.

pi.on("session_shutdown", async (event, ctx) => {
  // event.reason - "quit" | "reload" | "new" | "resume" | "fork"
  // event.targetSessionFile - destination session for session replacement flows
  // Cleanup, save state, etc.
});

Acara Agen

sebelum_agen_mulai

Dipecat setelah pengguna mengirimkan prompt, sebelum loop agen. Dapat memasukkan pesan dan/atau memodifikasi prompt sistem.

pi.on("before_agent_start", async (event, ctx) => {
  // event.prompt - user's prompt text
  // event.images - attached images (if any)
  // event.systemPrompt - current chained system prompt for this handler
  //   (includes changes from earlier before_agent_start handlers)
  // event.systemPromptOptions - structured options used to build the system prompt
  //   .customPrompt - any custom system prompt (from --system-prompt, SYSTEM.md, or custom templates)
  //   .selectedTools - tools currently active in the prompt
  //   .toolSnippets - one-line descriptions for each tool
  //   .promptGuidelines - custom guideline bullets
  //   .appendSystemPrompt - text from --append-system-prompt flags
  //   .cwd - working directory
  //   .contextFiles - AGENTS.md files and other loaded context files
  //   .skills - loaded skills

  return {
    // Inject a persistent message (stored in session, sent to LLM)
    message: {
      customType: "my-extension",
      content: "Additional context for the LLM",
      display: true,
    },
    // Replace the system prompt for this turn (chained across extensions)
    systemPrompt: event.systemPrompt + "\n\nExtra instructions for this turn...",
  };
});

Bidang systemPromptOptions memberikan ekstensi akses ke data terstruktur yang sama yang digunakan Pi untuk membuat perintah sistem. Hal ini memungkinkan Anda memeriksa apa yang Pi telah dimuat — perintah khusus, pedoman, cuplikan alat, context files, keterampilan — tanpa menemukan kembali sumber daya atau menguraikan ulang tanda. Gunakan saat ekstensi Anda perlu membuat perubahan mendalam dan terinformasi pada perintah sistem dengan tetap menghormati konfigurasi yang disediakan pengguna.

Di dalam before_agent_start, event.systemPrompt dan ctx.getSystemPrompt() keduanya mencerminkan perintah sistem berantai pada pengendali saat ini. Nanti before_agent_start penangan masih bisa memodifikasinya lagi.

agen_mulai / agen_end / agen_settled

agent_start terpicu saat proses agen tingkat rendah dimulai. agent_end terpicu saat proses tersebut berakhir, namun Pi masih dapat mencoba ulang secara otomatis, memadatkan otomatis, dan mencoba lagi, atau melanjutkan dengan pesan tindak lanjut yang antri. Gunakan agent_settled untuk integrasi status yang perlu diketahui Pi tidak akan terus berjalan secara otomatis.

pi.on("agent_start", async (_event, ctx) => {});

pi.on("agent_end", async (event, ctx) => {
  // event.messages - messages from this low-level run
});

pi.on("agent_settled", async (_event, ctx) => {
  // ctx.isIdle() is true here unless another extension started a new run.
});

turn_start / turn_end

Dipecat untuk setiap giliran (satu respons LLM + panggilan alat).

pi.on("turn_start", async (event, ctx) => {
  // event.turnIndex, event.timestamp
});

pi.on("turn_end", async (event, ctx) => {
  // event.turnIndex, event.message, event.toolResults
});

pesan_mulai / pembaruan_pesan / pesan_akhir

Diaktifkan karena pembaruan siklus hidup pesan.

  • message_start dan message_end diaktifkan untuk pesan pengguna, asisten, dan toolResult.
  • message_update diaktifkan untuk pembaruan streaming asisten.
  • message_end penangan dapat mengembalikan { message } untuk menggantikan pesan yang telah diselesaikan. Penggantinya harus tetap sama 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

Diaktifkan karena pembaruan siklus hidup eksekusi alat.

Dalam mode alat paralel:

  • tool_execution_start dipancarkan dalam urutan sumber asisten selama fase pra-penerbangan
  • tool_execution_update peristiwa mungkin disisipkan di seluruh alat
  • tool_execution_end dikeluarkan dalam urutan penyelesaian alat setelah setiap alat diselesaikan
  • peristiwa pesan toolResult terakhir masih dipancarkan kemudian dalam urutan sumber asisten
pi.on("tool_execution_start", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args
});

pi.on("tool_execution_update", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.args, event.partialResult
});

pi.on("tool_execution_end", async (event, ctx) => {
  // event.toolCallId, event.toolName, event.result, event.isError
});

konteks

Dipecat sebelum setiap panggilan LLM. Ubah pesan secara non-destruktif. Lihat Session Format untuk jenis pesan.

pi.on("context", async (event, ctx) => {
  // event.messages - deep copy, safe to modify
  const filtered = event.messages.filter(m => !shouldPrune(m));
  return { messages: filtered };
});

sebelum_penyedia_header

Diaktifkan setelah header HTTP keluar dipasang. Gunakan untuk menambah, mengganti, atau menghapus header permintaan.

Penangan bermutasi event.headers di tempatnya. Tetapkan kunci pada string untuk menambah atau menggantinya, atau ke null untuk menghapusnya.

pi.on("before_provider_headers", (event, ctx) => {
  // Add or override — e.g. a session id for gateway tracing/attribution
  event.headers["x-session-id"] = ctx.sessionManager.getSessionId();

  // Drop a tracking header pi adds for this call
  event.headers["X-OpenRouter-Title"] = null;
});

Berjalan sekali per permintaan penyedia; percobaan ulang menggunakan kembali header yang sama daripada menembakkan kembali hook.

sebelum_penyedia_permintaan

Diaktifkan setelah payload khusus penyedia dibuat, tepat sebelum permintaan dikirim. Penangan dijalankan dalam urutan pemuatan ekstensi. Mengembalikan undefined membuat payload tidak berubah. Mengembalikan nilai lain akan menggantikan payload untuk penangan selanjutnya dan untuk permintaan sebenarnya.

Kait ini dapat menulis ulang instruksi sistem tingkat penyedia atau menghapusnya seluruhnya. Perubahan tingkat muatan tersebut tidak tercermin oleh ctx.getSystemPrompt(), yang melaporkan string perintah sistem Pi dan bukan muatan penyedia serial akhir.

pi.on("before_provider_request", (event, ctx) => {
  console.log(JSON.stringify(event.payload, null, 2));

  // Optional: replace payload
  // return { ...event.payload, temperature: 0 };
});

Ini terutama berguna untuk men-debug serialisasi penyedia dan perilaku cache.

after_provider_response

Diaktifkan setelah respons HTTP diterima dan sebelum isi alirannya digunakan. Penangan dijalankan dalam urutan pemuatan ekstensi.

pi.on("after_provider_response", (event, ctx) => {
  // event.status - HTTP status code
  // event.headers - normalized response headers
  if (event.status === 429) {
    console.log("rate limited", event.headers["retry-after"]);
  }
});

Ketersediaan header tergantung pada penyedia dan transportasi. Providers bahwa respons HTTP abstrak tidak boleh mengekspos header.

Acara Model

model_pilih

Diaktifkan ketika model berubah melalui perintah /model, perputaran model (Ctrl+P), atau pemulihan sesi.

pi.on("model_select", async (event, ctx) => {
  // event.model - newly selected model
  // event.previousModel - previous model (undefined if first selection)
  // event.source - "set" | "cycle" | "restore"

  const prev = event.previousModel
    ? `${event.previousModel.provider}/${event.previousModel.id}`
    : "none";
  const next = `${event.model.provider}/${event.model.id}`;

  ctx.ui.notify(`Model changed (${event.source}): ${prev} -> ${next}`, "info");
});

Gunakan ini untuk memperbarui elemen UI (bilah status, footer) atau melakukan inisialisasi khusus model saat model aktif berubah.

berpikir_tingkat_pilih

Dipecat ketika tingkat berpikir berubah. Ini hanya untuk pemberitahuan; nilai pengembalian handler diabaikan.

pi.on("thinking_level_select", async (event, ctx) => {
  // event.level - newly selected thinking level
  // event.previousLevel - previous thinking level

  ctx.ui.setStatus("thinking", `thinking: ${event.level}`);
});

Gunakan ini untuk memperbarui UI ekstensi ketika pi.setThinkingLevel(), perubahan model, atau kontrol tingkat berpikir bawaan mengubah tingkat berpikir aktif.

Acara Alat

alat_panggilan

Diaktifkan setelah tool_execution_start, sebelum alat dijalankan. Dapat memblokir. Gunakan isToolCallEventType untuk mempersempit dan mendapatkan masukan yang diketik.

Sebelum tool_call berjalan, pi menunggu peristiwa Agen yang dipancarkan sebelumnya selesai dikuras melalui AgentSession. Ini berarti ctx.sessionManager diperbarui melalui pesan pemanggil alat asisten saat ini.

Dalam mode eksekusi alat paralel default, panggilan alat saudara dari pesan asisten yang sama dipra-penerbangan secara berurutan, lalu dieksekusi secara bersamaan. tool_call tidak dijamin melihat hasil alat saudara dari pesan asisten yang sama di ctx.sessionManager.

event.input bisa berubah. Mutasi di tempatnya untuk menambal argumen alat sebelum dieksekusi.

Jaminan perilaku:

  • Mutasi ke event.input mempengaruhi eksekusi alat sebenarnya
  • Penangan tool_call kemudian melihat mutasi yang dilakukan oleh penangan sebelumnya
  • Tidak ada validasi ulang yang dilakukan setelah mutasi Anda
  • Kembalikan nilai dari tool_call pemblokiran kontrol melalui { block: true, reason?: string, terminate?: boolean }
  • terminate hanya berlaku untuk panggilan yang diblokir; agen berhenti lebih awal hanya ketika setiap hasil akhir dalam batch dihentikan
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";

pi.on("tool_call", async (event, ctx) => {
  // event.toolName - "bash", "read", "write", "edit", etc.
  // event.toolCallId
  // event.input - tool parameters (mutable)

  // Built-in tools: no type params needed
  if (isToolCallEventType("bash", event)) {
    // event.input is { command: string; timeout?: number }
    event.input.command = `source ~/.profile\n${event.input.command}`;

    if (event.input.command.includes("rm -rf")) {
      return { block: true, reason: "Dangerous command", terminate: true };
    }
  }

  if (isToolCallEventType("read", event)) {
    // event.input is { path: string; offset?: number; limit?: number }
    console.log(`Reading: ${event.input.path}`);
  }
});

Mengetik masukan alat khusus

Alat khusus harus mengekspor jenis masukannya:

// my-extension.ts
export type MyToolInput = Static<typeof myToolSchema>;

Gunakan isToolCallEventType dengan parameter tipe eksplisit:

import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
import type { MyToolInput } from "my-extension";

pi.on("tool_call", (event) => {
  if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
    event.input.action;  // typed
  }
});

alat_hasil

Diaktifkan setelah eksekusi alat selesai dan sebelum tool_execution_end ditambah peristiwa pesan hasil alat akhir dikeluarkan. Dapat mengubah hasil.

Dalam mode pahat paralel, tool_result dan tool_execution_end dapat disisipkan dalam urutan penyelesaian pahat, sedangkan kejadian pesan akhir toolResult masih dikirimkan kemudian dalam urutan sumber asisten.

tool_result rantai penangan seperti middleware:

  • Penangan dijalankan dalam urutan pemuatan ekstensi
  • Setiap penangan melihat hasil terbaru setelah penangan sebelumnya berubah
  • Penangan dapat mengembalikan sebagian patch (content, details, isError, atau usage); bidang yang dihilangkan mempertahankan nilainya saat ini

Gunakan ctx.signal untuk pekerjaan asinkron bersarang di dalam pengendali. Hal ini memungkinkan Esc membatalkan panggilan model, fetch(), dan operasi sadar pembatalan lainnya yang dimulai oleh ekstensi.

import { isBashToolResult } from "@earendil-works/pi-coding-agent";

pi.on("tool_result", async (event, ctx) => {
  // event.toolName, event.toolCallId, event.input
  // event.content, event.details, event.isError, event.usage

  if (isBashToolResult(event)) {
    // event.details is typed as BashToolDetails
  }

  const response = await fetch("https://example.com/summarize", {
    method: "POST",
    body: JSON.stringify({ content: event.content }),
    signal: ctx.signal,
  });

  // Modify result:
  return { content: [...], details: {...}, isError: false, usage: nestedModelUsage };
});

Acara Pesta Pengguna

pengguna_bash

Diaktifkan ketika pengguna menjalankan perintah ! atau !!. Dapat mencegat.

import { createLocalBashOperations } from "@earendil-works/pi-coding-agent";

pi.on("user_bash", (event, ctx) => {
  // event.command - the bash command
  // event.excludeFromContext - true if !! prefix
  // event.cwd - working directory

  // Option 1: Provide custom operations (e.g., SSH)
  return { operations: remoteBashOps };

  // Option 2: Wrap pi's built-in local bash backend
  const local = createLocalBashOperations();
  return {
    operations: {
      exec(command, cwd, options) {
        return local.exec(`source ~/.profile\n${command}`, cwd, options);
      }
    }
  };

  // Option 3: Full replacement - return result directly
  return { result: { output: "...", exitCode: 0, cancelled: false, truncated: false } };
});

Masukan Acara

masukan

Dipicu ketika masukan pengguna diterima, setelah perintah ekstensi diperiksa tetapi sebelum perluasan keterampilan dan templat. Acara ini melihat teks masukan mentah, jadi /skill:foo dan /template belum diperluas.

Pemrosesan pesanan:

  1. Perintah ekstensi (/cmd) diperiksa terlebih dahulu - jika ditemukan, handler dijalankan dan event input dilewati
  2. input peristiwa kebakaran - dapat mencegat, mengubah, atau menangani
  3. Jika tidak ditangani: perintah keterampilan (/skill:name) diperluas ke konten keterampilan
  4. Jika tidak ditangani: prompt templates (/template) diperluas ke konten templat
  5. Pemrosesan agen dimulai (before_agent_start, dll.)
pi.on("input", async (event, ctx) => {
  // event.text - raw input (before skill/template expansion)
  // event.images - attached images, if any
  // event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
  // event.streamingBehavior - "steer" | "followUp" | undefined
  //   undefined when idle, "steer" for mid-stream interrupts,
  //   "followUp" for messages queued until the agent finishes

  // Transform: rewrite input before expansion
  if (event.text.startsWith("?quick "))
    return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };

  // Handle: respond without LLM (extension shows its own feedback)
  if (event.text === "ping") {
    ctx.ui.notify("pong", "info");
    return { action: "handled" };
  }

  // Route by source: skip processing for extension-injected messages
  if (event.source === "extension") return { action: "continue" };

  // Intercept skill commands before expansion
  if (event.text.startsWith("/skill:")) {
    // Could transform, block, or let pass through
  }

  return { action: "continue" };  // Default: pass through to expansion
});

Hasil:

  • continue - melewati tanpa perubahan (default jika handler tidak mengembalikan apa pun)
  • transform - ubah teks/gambar, lalu lanjutkan perluasan
  • handled - lewati agen sepenuhnya (penangan pertama yang mengembalikan ini menang)

Mengubah rantai di seluruh penangan. Lihat input-transform.ts dan input-transform-streaming.ts untuk perutean streamingBehavior.

Konteks Ekstensi

Semua penangan menerima ctx: ExtensionContext.

ctx.ui

Metode UI untuk interaksi pengguna. Lihat Custom UI untuk detail selengkapnya.

ctx.mode

Mode lari saat ini: "tui", "rpc", "json", atau "print". Gunakan ctx.mode === "tui" untuk menjaga fitur khusus terminal seperti custom(), pabrik komponen, input terminal, dan rendering TUI langsung.

ctx.hasUI

true dalam mode TUI dan RPC. false dalam mode cetak (-p) dan mode JSON. Gunakan ini untuk menjaga metode dialog (select, confirm, input, editor) dan metode api-dan-lupakan (notify, setStatus, setWidget, setTitle, setEditorText) yang berfungsi baik di TUI maupun RPC mode. Dalam mode RPC, beberapa metode khusus TUI tidak dapat dioperasikan atau dikembalikan secara default (lihat rpc.md).

ctx.cwd

Direktori kerja saat ini.

Gunakan CONFIG_DIR_NAME alih-alih melakukan hardcoding .pi saat membuat jalur konfigurasi proyek-lokal. Distribusi yang diganti mereknya dapat menggunakan nama direktori konfigurasi yang berbeda.

import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { join } from "node:path";

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    const projectConfigPath = join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json");
    // ...
  });
}

ctx.isProjectTrusted()

Mengembalikan apakah kepercayaan proyek-lokal aktif untuk konteks sesi saat ini. Hal ini mencakup keputusan perwalian sementara dan CLI pengesampingan perwalian, bukan hanya keputusan yang disimpan dalam penyimpanan perwalian global.

Gunakan ini sebelum membaca konfigurasi ekstensi proyek-lokal yang hanya berlaku untuk proyek tepercaya.

ctx.sessionManager

Akses hanya baca ke status sesi. Lihat Session Format untuk SessionManager API lengkap dan tipe entri.

Untuk tool_call, status ini disinkronkan melalui pesan asisten saat ini sebelum penangan dijalankan. Dalam mode eksekusi alat paralel, masih belum ada jaminan untuk menyertakan hasil alat saudara dari pesan asisten yang sama.

ctx.sessionManager.getEntries()             // All entries
ctx.sessionManager.getBranch()              // Current branch
ctx.sessionManager.buildContextEntries()    // Active branch entries with compaction applied
ctx.sessionManager.getLeafId()              // Current leaf entry ID

ctx.modelRegistry / ctx.model / ctx.thinkingLevel / ctx.scopedModels

Akses ke model, penyedia, dan autentikasi terselesaikan. ctx.modelRegistry.getProvider(id) mengembalikan penyedia pi-ai yang efektif, sementara getProviderAuth(id) menyelesaikan API key saat ini, header, URL dasar, dan lingkungan cakupan penyedia tanpa memerlukan model yang dimuat. ctx.model adalah model aktif, dan ctx.thinkingLevel adalah tingkat berpikir efektif saat ini.

ctx.scopedModels adalah daftar model baca-saja yang tercakup dalam sesi saat ini — kumpulan yang sama yang ditampilkan oleh perintah /scoped-models. Ini diselesaikan pada awal sesi dari bendera --models CLI dan pengaturan enabledModels (dicocokkan dengan katalog yang tersedia dengan minimatch di provider/modelId atau modelId kosong). Ini kosong jika tidak ada pelingkupan yang dikonfigurasi, artinya setiap model yang tersedia dapat digunakan. Setiap entri adalah { model, thinkingLevel? }, dengan thinkingLevel diatur hanya ketika pola menyematkannya (misalnya anthropic/*:high). Gunakan ini untuk mengisi pemilih model yang mencerminkan pemilih model bawaan, alih-alih menghitung seluruh katalog melalui ctx.modelRegistry.getAvailable().

ctx.signal

Sinyal pembatalan agen saat ini, atau undefined ketika tidak ada giliran agen yang aktif.

Gunakan ini untuk pekerjaan bersarang yang sadar akan pembatalan yang dimulai oleh penangan ekstensi, misalnya:

  • fetch(..., { signal: ctx.signal })
  • panggilan model yang menerima signal
  • file atau pembantu proses yang menerima AbortSignal

ctx.signal biasanya ditentukan selama event giliran aktif seperti tool_call, tool_result, message_update, dan turn_end. Biasanya undefined dalam konteks idle atau non-turn seperti acara sesi, perintah ekstensi, dan pintasan diaktifkan saat pi idle.

pi.on("tool_result", async (event, ctx) => {
  const response = await fetch("https://example.com/api", {
    method: "POST",
    body: JSON.stringify(event),
    signal: ctx.signal,
  });

  const data = await response.json();
  return { details: data };
});

ctx.isIdle() / ctx.abort() / ctx.hasPendingMessages()

Kontrol aliran pembantu. ctx.isIdle() salah saat Pi sedang memproses proses agen, percobaan ulang otomatis, percobaan pemadatan otomatis, atau kelanjutan antrean.

ctx.shutdown()

Minta penutupan pi dengan baik.

  • Mode interaktif: Ditunda hingga agen menganggur (setelah memproses semua pesan kemudi dan tindak lanjut yang diantri).
  • RPC mode: Ditunda hingga status siaga berikutnya (setelah menyelesaikan respons perintah saat ini, saat menunggu perintah berikutnya).
  • Mode cetak: Tanpa pengoperasian. Proses keluar secara otomatis ketika semua perintah diproses.

Memancarkan acara session_shutdown ke semua ekstensi sebelum keluar. Tersedia dalam semua konteks (event handler, alat, perintah, pintasan).

pi.on("tool_call", (event, ctx) => {
  if (isFatal(event.input)) {
    ctx.shutdown();
  }
});

ctx.getContextUsage()

Mengembalikan penggunaan konteks saat ini untuk model aktif. Menggunakan penggunaan asisten terakhir bila tersedia, lalu memperkirakan token untuk pesan tambahan.

const usage = ctx.getContextUsage();
if (usage && usage.tokens > 100_000) {
  // ...
}

ctx.kompak()

Memicu pemadatan tanpa menunggu selesai. Gunakan onComplete dan onError untuk tindakan tindak lanjut.

ctx.compact({
  customInstructions: "Focus on recent changes",
  onComplete: (result) => {
    ctx.ui.notify("Compaction completed", "info");
  },
  onError: (error) => {
    ctx.ui.notify(`Compaction failed: ${error.message}`, "error");
  },
});

ctx.getSystemPrompt()

Mengembalikan string perintah sistem Pi saat ini.

  • Selama before_agent_start, hal ini mencerminkan perubahan cepat sistem berantai yang dilakukan sejauh ini untuk belokan saat ini.
  • Ini tidak termasuk mutasi pesan context selanjutnya.
  • Ini tidak termasuk before_provider_request penulisan ulang payload.
  • Jika ekstensi yang dimuat kemudian dijalankan setelah ekstensi Anda, ekstensi tersebut masih dapat mengubah ekstensi yang dikirimkan.
pi.on("before_agent_start", (event, ctx) => {
  const prompt = ctx.getSystemPrompt();
  console.log(`System prompt length: ${prompt.length}`);
});

EkstensiPerintahKonteks

Penangan perintah menerima ExtensionCommandContext, yang diperluas ExtensionContext dengan metode kontrol sesi. Ini hanya tersedia dalam perintah karena dapat menemui jalan buntu jika dipanggil dari event handler.

ctx.getSystemPromptOptions()

Mengembalikan input dasar Pi yang saat ini digunakan untuk membangun prompt sistem.

const options = ctx.getSystemPromptOptions();
const contextPaths = options.contextFiles?.map((file) => file.path) ?? [];

Ini memiliki bentuk dan kemampuan berubah yang sama dengan before_agent_start event.systemPromptOptions: perintah khusus, alat aktif, cuplikan alat, pedoman perintah, teks perintah sistem yang ditambahkan, cwd, context files yang dimuat, dan keterampilan yang dimuat. Ini mungkin berisi konten file konteks penuh, jadi perlakukan itu sebagai data lokal ekstensi yang sensitif dan hindari memaparkannya melalui daftar perintah, log, atau metadata pelengkapan otomatis.

Ini melaporkan input prompt dasar saat ini. Ini tidak termasuk perubahan cepat sistem berantai before_agent_start per putaran, mutasi pesan peristiwa context di kemudian hari, atau penulisan ulang muatan before_provider_request.

ctx.waitForIdle()

Tunggu hingga agen menyelesaikan sepenuhnya, termasuk percobaan ulang otomatis, percobaan pemadatan otomatis, dan kelanjutan antrean:

pi.registerCommand("my-cmd", {
  handler: async (args, ctx) => {
    await ctx.waitForIdle();
    // Agent is now idle, safe to modify session
  },
});

ctx.sesi baru(pilihan?)

Buat sesi baru:

const parentSession = ctx.sessionManager.getSessionFile();
const kickoff = "Continue in the replacement session";

const result = await ctx.newSession({
  parentSession,
  setup: async (sm) => {
    sm.appendMessage({
      role: "user",
      content: [{ type: "text", text: "Context from previous session..." }],
      timestamp: Date.now(),
    });
  },
  withSession: async (ctx) => {
    // Use only the replacement-session ctx here.
    await ctx.sendUserMessage(kickoff);
  },
});

if (result.cancelled) {
  // An extension cancelled the new session
}

Pilihan:

  • parentSession: file sesi induk untuk direkam di header sesi baru
  • setup: mutasikan SessionManager sesi baru sebelum withSession berjalan
  • withSession: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakan pi / perintah ctx lama yang diambil; lihat Session replacement lifecycle and footguns.

ctx.fork(entryId, opsi?)

Cabang dari entri tertentu, membuat file sesi baru:

const result = await ctx.fork("entry-id-123", {
  withSession: async (ctx) => {
    // Use only the replacement-session ctx here.
    ctx.ui.notify("Now in the forked session", "info");
  },
});
if (result.cancelled) {
  // An extension cancelled the fork
}

const cloneResult = await ctx.fork("entry-id-456", { position: "at" });
if (cloneResult.cancelled) {
  // An extension cancelled the clone
}

Pilihan:

  • position: "before" (default) bercabang sebelum pesan pengguna yang dipilih, mengembalikan prompt itu ke editor
  • position: "at" menduplikasi jalur aktif melalui entri yang dipilih tanpa memulihkan teks editor
  • withSession: menjalankan pekerjaan pasca peralihan dengan konteks sesi penggantian yang baru. Jangan gunakan pi / perintah ctx lama yang diambil; lihat Session replacement lifecycle and footguns.

ctx.navigateTree(targetId, opsi?)

Arahkan ke titik lain di session tree:

const result = await ctx.navigateTree("entry-id-456", {
  summarize: true,
  customInstructions: "Focus on error handling changes",
  replaceInstructions: false, // true = replace default prompt entirely
  label: "review-checkpoint",
});

Pilihan:

  • summarize: Apakah akan membuat ringkasan cabang yang ditinggalkan
  • customInstructions: Instruksi khusus untuk peringkas
  • replaceInstructions: Jika benar, customInstructions menggantikan prompt default dan bukannya ditambahkan
  • label: Label untuk dilampirkan pada entri ringkasan cabang (atau entri target jika tidak diringkas)

ctx.switchSession(sessionPath, opsi?)

Beralih ke file sesi lain:

const result = await ctx.switchSession("/path/to/session.jsonl", {
  withSession: async (ctx) => {
    await ctx.sendUserMessage("Resume work in the replacement session");
  },
});
if (result.cancelled) {
  // An extension cancelled the switch via session_before_switch
}

Pilihan:

Untuk menemukan sesi yang tersedia, gunakan metode statis SessionManager.list() atau SessionManager.listAll():

import { SessionManager } from "@earendil-works/pi-coding-agent";

pi.registerCommand("switch", {
  description: "Switch to another session",
  handler: async (args, ctx) => {
    const sessions = await SessionManager.list(ctx.cwd);
    if (sessions.length === 0) return;
    const choice = await ctx.ui.select(
      "Pick session:",
      sessions.map(s => s.file),
    );
    if (choice) {
      await ctx.switchSession(choice, {
        withSession: async (ctx) => {
          ctx.ui.notify("Switched session", "info");
        },
      });
    }
  },
});

Siklus hidup dan footgun penggantian sesi

withSession menerima ReplacedSessionContext baru, yang memperluas ExtensionCommandContext dengan pembantu asinkron sendMessage() dan sendUserMessage() yang terikat pada sesi penggantian.

Siklus hidup dan footgun:

  • withSession berjalan hanya setelah sesi lama memancarkan session_shutdown, runtime lama telah dihapus, sesi pengganti telah di-rebound, dan instance ekstensi baru telah menerima session_start.
  • Callback masih dijalankan di penutupan asli, bukan di dalam instance ekstensi baru. Itu berarti instance ekstensi lama Anda mungkin sudah menjalankan pembersihan penutupannya sebelum withSession dimulai.
  • Objek terikat sesi pi / perintah lama ctx lama yang diambil akan menjadi basi setelah diganti dan akan dibuang jika digunakan. Gunakan hanya ctx yang diteruskan ke withSession untuk pekerjaan terikat sesi.
  • Benda mentah yang diekstraksi sebelumnya tetap menjadi tanggung jawab Anda. Misalnya, jika Anda menangkap const sm = ctx.sessionManager sebelum penggantian, sm tetap menjadi objek SessionManager yang lama. Jangan menggunakannya kembali setelah penggantian.
  • Kode di withSession harus mengasumsikan status apa pun yang dibatalkan oleh pengendali session_shutdown Anda sudah hilang. Hanya ambil data biasa yang bertahan saat dimatikan dengan bersih, seperti string, id, dan konfigurasi serial.

Pola aman:

pi.registerCommand("handoff", {
  handler: async (_args, ctx) => {
    const kickoff = "Continue from the replacement session";
    await ctx.newSession({
      withSession: async (ctx) => {
        await ctx.sendUserMessage(kickoff);
      },
    });
  },
});

Pola tidak aman:

pi.registerCommand("handoff", {
  handler: async (_args, ctx) => {
    const oldSessionManager = ctx.sessionManager;
    await ctx.newSession({
      withSession: async (_ctx) => {
        // stale old objects: do not do this
        oldSessionManager.getSessionFile();
        pi.sendUserMessage("wrong");
      },
    });
  },
});

ctx.reload()

Jalankan alur isi ulang yang sama seperti /reload.

pi.registerCommand("reload-runtime", {
  description: "Reload extensions, skills, prompts, themes, and context files",
  handler: async (_args, ctx) => {
    await ctx.reload();
    return;
  },
});

Perilaku penting:

  • await ctx.reload() memancarkan session_shutdown untuk waktu proses ekstensi saat ini
  • Kemudian memuat ulang sumber daya dan mengeluarkan session_start dengan reason: "reload" dan resources_discover dengan alasan "reload"
  • Pengendali perintah yang sedang berjalan masih berlanjut di bingkai panggilan lama
  • Kode setelah await ctx.reload() masih berjalan dari versi pra-muat ulang
  • Kode setelah await ctx.reload() tidak boleh menganggap status ekstensi dalam memori yang lama masih valid
  • Setelah handler kembali, perintah/peristiwa/panggilan alat di masa mendatang menggunakan versi ekstensi baru

Untuk perilaku yang dapat diprediksi, perlakukan reload sebagai terminal untuk pengendali tersebut (await ctx.reload(); return;).

Alat dijalankan dengan ExtensionContext, sehingga tidak dapat memanggil ctx.reload() secara langsung. Gunakan perintah sebagai titik masuk muat ulang, lalu tampilkan alat yang mengantri perintah tersebut sebagai pesan pengguna tindak lanjut.

Contoh alat yang dapat dipanggil LLM untuk memicu pemuatan ulang:

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

export default function (pi: ExtensionAPI) {
  pi.registerCommand("reload-runtime", {
    description: "Reload extensions, skills, prompts, themes, and context files",
    handler: async (_args, ctx) => {
      await ctx.reload();
      return;
    },
  });

  pi.registerTool({
    name: "reload_runtime",
    label: "Reload Runtime",
    description: "Reload extensions, skills, prompts, themes, and context files",
    parameters: Type.Object({}),
    async execute() {
      pi.sendUserMessage("/reload-runtime", { deliverAs: "followUp" });
      return {
        content: [{ type: "text", text: "Queued /reload-runtime as a follow-up command." }],
      };
    },
  });
}

EkstensiAPI Metode

pi.on(acara, pengendali)

Berlangganan acara. Lihat Events untuk jenis peristiwa dan nilai kembalian.

pi.registerTool(definisi)

Daftarkan alat khusus yang dapat dipanggil oleh LLM. Lihat Custom Tools untuk detail selengkapnya.

pi.registerTool() berfungsi selama pemuatan ekstensi dan setelah pengaktifan. Anda dapat memanggilnya di dalam session_start, pengendali perintah, atau pengendali kejadian lainnya. Alat baru segera disegarkan di sesi yang sama, sehingga muncul di pi.getAllTools() dan dapat dipanggil oleh LLM tanpa /reload.

Gunakan pi.setActiveTools() untuk mengaktifkan atau menonaktifkan alat (termasuk alat yang ditambahkan secara dinamis) saat runtime.

Gunakan promptSnippet untuk memasukkan alat khusus ke dalam entri satu baris di Available tools, dan promptGuidelines untuk menambahkan poin khusus alat ke bagian Guidelines default saat alat aktif.

Penting: promptGuidelines poin ditambahkan rata ke bagian Guidelines tanpa awalan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari "Gunakan alat ini ketika..." karena LLM tidak dapat membedakan alat mana yang dimaksud dengan "ini". Tulis "Gunakan my_tool ketika..." sebagai gantinya.

Lihat dynamic-tools.ts untuk contoh selengkapnya.

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "What this tool does",
  promptSnippet: "Summarize or transform text according to action",
  promptGuidelines: ["Use my_tool when the user asks to summarize previously generated text."],
  parameters: Type.Object({
    action: StringEnum(["list", "add"] as const),
    text: Type.Optional(Type.String()),
  }),
  prepareArguments(args) {
    // Optional compatibility shim. Runs before schema validation.
    // Return the current schema shape, for example to fold legacy fields
    // into the modern parameter object.
    return args;
  },

  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // Stream progress
    onUpdate?.({ content: [{ type: "text", text: "Working..." }] });

    return {
      content: [{ type: "text", text: "Done" }],
      details: { result: "..." },
    };
  },

  // Optional: Custom rendering
  renderCall(args, theme, context) { ... },
  renderResult(result, options, theme, context) { ... },
});

pi.sendMessage(pesan, opsi?)

Masukkan pesan khusus ke dalam sesi. Pesan khusus berpartisipasi dalam konteks LLM. Untuk konten tahan lama TUI saja yang tidak boleh dikirim ke LLM, gunakan pi.appendEntry() dengan pi.registerEntryRenderer().

pi.sendMessage({
  customType: "my-extension",
  content: "Message text",
  display: true,
  details: { ... },
}, {
  triggerTurn: true,
  deliverAs: "steer",
});

Pilihan:

  • deliverAs - Modus pengiriman:
    • "steer" (default) - Mengantrekan pesan saat streaming. Dikirim setelah giliran asisten saat ini selesai menjalankan panggilan alatnya, sebelum panggilan LLM berikutnya.
    • "followUp" - Menunggu agen selesai. Dikirim hanya ketika agen tidak lagi memiliki panggilan alat.
    • "nextTurn" - Mengantri untuk permintaan pengguna berikutnya. Tidak mengganggu atau memicu apa pun.
  • triggerTurn: true - Jika agen menganggur, segera picu respons LLM. Hanya berlaku untuk mode "steer" dan "followUp" (diabaikan untuk "nextTurn").

pi.sendUserMessage(konten, opsi?)

Kirim pesan pengguna ke agen. Berbeda dengan sendMessage() yang mengirimkan pesan khusus, ini mengirimkan pesan pengguna sebenarnya yang tampak seolah-olah diketik oleh pengguna. Selalu memicu belokan.

// Simple text message
pi.sendUserMessage("What is 2+2?");

// With content array (text + images)
pi.sendUserMessage([
  { type: "text", text: "Describe this image:" },
  { type: "image", source: { type: "base64", mediaType: "image/png", data: "..." } },
]);

// During streaming - must specify delivery mode
pi.sendUserMessage("Focus on error handling", { deliverAs: "steer" });
pi.sendUserMessage("And then summarize", { deliverAs: "followUp" });

Pilihan:

  • deliverAs - Diperlukan saat agen sedang streaming:
    • "steer" - Mengantri pesan untuk dikirimkan setelah giliran asisten saat ini selesai menjalankan panggilan alatnya
    • "followUp" - Menunggu agen menyelesaikan semua alat

Saat tidak streaming, pesan langsung terkirim dan memicu giliran baru. Saat streaming tanpa deliverAs, terjadi kesalahan.

Lihat send-user-message.ts untuk contoh lengkap.

pi.appendEntry(Tipe khusus, data?)

Pertahankan data ekstensi. Entri khusus TIDAK berpartisipasi dalam konteks LLM. Dalam mode interaktif, mereka juga dapat merender di dalam transkrip obrolan saat dipasangkan dengan pi.registerEntryRenderer().

pi.appendEntry("my-state", { count: 42 });
pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

// Restore on reload
pi.on("session_start", async (_event, ctx) => {
  for (const entry of ctx.sessionManager.getEntries()) {
    if (entry.type === "custom" && entry.customType === "my-state") {
      // Reconstruct from entry.data
    }
  }
});

pi.setSessionName(nama)

Tetapkan nama tampilan sesi (ditampilkan di pemilih sesi, bukan di pesan pertama).

pi.setSessionName("Refactor auth module");

pi.getSessionName()

Dapatkan nama sesi saat ini, jika disetel.

const name = pi.getSessionName();
if (name) {
  console.log(`Session: ${name}`);
}

pi.setLabel(entryId, label)

Menetapkan atau menghapus label pada entri. Label adalah penanda yang ditentukan pengguna untuk bookmark dan navigasi (ditampilkan di pemilih /tree).

// Set a label
pi.setLabel(entryId, "checkpoint-before-refactor");

// Clear a label
pi.setLabel(entryId, undefined);

// Read labels via sessionManager
const label = ctx.sessionManager.getLabel(entryId);

Label tetap ada dalam sesi dan bertahan saat dimulai ulang. Gunakan mereka untuk menandai titik-titik penting (belokan, pos pemeriksaan) di pohon percakapan.

pi.registerCommand(nama, opsi)

Daftarkan perintah.

Jika beberapa ekstensi mendaftarkan nama perintah yang sama, pi menyimpan semuanya dan menetapkan sufiks pemanggilan numerik dalam urutan pemuatan, misalnya /review:1 dan /review:2.

pi.registerCommand("stats", {
  description: "Show session statistics",
  handler: async (args, ctx) => {
    const count = ctx.sessionManager.getEntries().length;
    ctx.ui.notify(`${count} entries`, "info");
  }
});

Opsional: tambahkan argumen pelengkapan otomatis untuk /command...:

import type { AutocompleteItem } from "@earendil-works/pi-tui";

pi.registerCommand("deploy", {
  description: "Deploy to an environment",
  getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
    const envs = ["dev", "staging", "prod"];
    const items = envs.map((e) => ({ value: e, label: e }));
    const filtered = items.filter((i) => i.value.startsWith(prefix));
    return filtered.length > 0 ? filtered : null;
  },
  handler: async (args, ctx) => {
    ctx.ui.notify(`Deploying: ${args}`, "info");
  },
});

pi.getCommands()

Dapatkan slash commands tersedia untuk pemanggilan melalui prompt di sesi saat ini. Termasuk perintah ekstensi, prompt templates, dan perintah keterampilan. Daftarnya cocok dengan urutan RPC get_commands: ekstensi terlebih dahulu, lalu templat, lalu keterampilan.

const commands = pi.getCommands();
const bySource = commands.filter((command) => command.source === "extension");
const userScoped = commands.filter((command) => command.sourceInfo.scope === "user");

Setiap entri memiliki bentuk ini:

{
  name: string; // Invokable command name without the leading slash. May be suffixed like "review:1"
  description?: string;
  source: "extension" | "prompt" | "skill";
  sourceInfo: {
    path: string;
    source: string;
    scope: "user" | "project" | "temporary";
    origin: "package" | "top-level";
    baseDir?: string;
  };
}

Gunakan sourceInfo sebagai bidang asal kanonik. Jangan menyimpulkan kepemilikan dari nama perintah atau dari penguraian jalur ad hoc.

Perintah interaktif bawaan (seperti /model dan /settings) tidak disertakan di sini. Mereka ditangani hanya secara interaktif mode dan tidak akan dijalankan jika dikirim melalui prompt.

pi.registerMessageRenderer(tipe khusus, penyaji)

Daftarkan penyaji TUI khusus untuk pesan khusus dengan customType Anda. Pesan khusus dibuat dengan pi.sendMessage() dan berpartisipasi dalam konteks LLM. Lihat Custom UI.

pi.registerMarkdownTransformator(transformator)

Daftarkan transformator untuk Markdown dalam teks pengguna normal, teks asisten, dan blok pemikiran. Trafo dijalankan dalam urutan beban ekstensi, dan setiap trafo menerima Markdown yang dikembalikan oleh trafo sebelumnya. Setelah rantai selesai, Pi merender konten yang diubah dengan penyaji bawaannya.

Transformator menerima string Markdown dan konteks dengan:

  • messageType"user", "assistant", atau "assistant-thinking"
  • isStreamingtrue untuk pembaruan sebagian asisten; false untuk pengguna, asisten yang diselesaikan, dan pesan yang dipulihkan
  • availableWidth — kolom terminal persis tersedia untuk konten Markdown yang diubah

Kembalikan Markdown yang telah diubah:

pi.registerMarkdownTransformer((markdown, { messageType, isStreaming }) => {
  if (isStreaming || messageType === "assistant-thinking") return markdown;
  return markdown.replaceAll("-->", "→");
});

Jika trafo mati, Pi mempertahankan Markdown yang dihasilkan sejauh ini dan dilanjutkan dengan trafo berikutnya. Pengaitnya hanya untuk tampilan: pesan asli tetap tidak berubah dalam konteks sesi dan model. Ini berjalan untuk pesan pengguna baru, pembaruan streaming asisten, pesan sesi yang dipulihkan, dan perubahan lebar terminal, sehingga transformator harus tetap sinkron dan murah.

pi.registerEntryRenderer(tipe khusus, penyaji)

Daftarkan penyaji TUI khusus untuk entri khusus dengan customType Anda. Entri khusus dibuat dengan pi.appendEntry() dan tidak berpartisipasi dalam konteks LLM.

import { Box, Text } from "@earendil-works/pi-tui";

pi.registerEntryRenderer("status-card", (entry, { expanded }, theme) => {
  const data = entry.data as { title: string; count: number };
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
  box.addChild(new Text(`${theme.bold(data.title)}: ${data.count}`));
  if (expanded) {
    box.addChild(new Text(theme.fg("dim", JSON.stringify(data, null, 2))));
  }
  return box;
});

pi.appendEntry("status-card", { title: "Indexed files", count: 17 });

pi.registerShortcut(pintasan, opsi)

Daftarkan pintasan keyboard. Lihat keybindings.md untuk format pintasan dan pengikatan tombol bawaan.

pi.registerShortcut("ctrl+shift+p", {
  description: "Toggle plan mode",
  handler: async (ctx) => {
    ctx.ui.notify("Toggled!");
  },
});

pi.registerFlag(nama, opsi)

Daftarkan bendera CLI.

pi.registerFlag("plan", {
  description: "Start in plan mode",
  type: "boolean",
  default: false,
});

// Check value
if (pi.getFlag("plan")) {
  // Plan mode enabled
}

pi.exec(perintah, argumen, opsi?)

Jalankan perintah shell.

const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killed

pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(nama)

Kelola alat aktif. Ini berfungsi untuk alat bawaan dan alat yang terdaftar secara dinamis. pi.getActiveTools() mengembalikan nama alat aktif sebagai string[]; pi.getAllTools() mengembalikan metadata untuk semua alat yang dikonfigurasi.

const active = pi.getActiveTools(); // ["read", "bash", ...]
const all = pi.getAllTools();
// all = [{
//   name: "read",
//   description: "Read file contents...",
//   parameters: ...,
//   promptGuidelines: ["Use read to examine files instead of cat or sed."],
//   sourceInfo: { path: "<builtin:read>", source: "builtin", scope: "temporary", origin: "top-level" }
// }, ...]
const builtinTools = all.filter((t) => t.sourceInfo.source === "builtin");
const extensionTools = all.filter((t) => t.sourceInfo.source !== "builtin" && t.sourceInfo.source !== "sdk");
pi.setActiveTools([...new Set([...active, "my_custom_tool"])]); // Keep current tools and enable my_custom_tool
pi.setActiveTools(["read", "bash"]); // Switch to read-only

pi.getAllTools() mengembalikan name, description, parameters, promptGuidelines, dan sourceInfo.

Nilai sourceInfo.source yang umum:

  • builtin untuk alat bawaan
  • sdk untuk alat yang diteruskan melalui createAgentSession({ customTools })
  • metadata sumber ekstensi untuk alat yang didaftarkan oleh ekstensi

pi.setModel(model)

Tetapkan model saat ini. Mengembalikan false jika tidak ada API key yang tersedia untuk model. Lihat models.md untuk mengonfigurasi model khusus.

const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
  const success = await pi.setModel(model);
  if (!success) {
    ctx.ui.notify("No API key for this model", "error");
  }
}

pi.getThinkingLevel() / pi.setThinkingLevel(tingkat)

Dapatkan atau atur tingkat berpikir. Level disesuaikan dengan kemampuan model (model non-penalaran selalu menggunakan "mati"). Perubahan memancarkan thinking_level_select.

const current = pi.getThinkingLevel();  // "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"
pi.setThinkingLevel("high");

pi.events

Bus acara bersama untuk komunikasi antar ekstensi:

pi.events.on("my:event", (data) => { ... });
pi.events.emit("my:event", { ... });

pi.registerProvider(nama, konfigurasi)

Daftarkan atau ganti penyedia model secara dinamis. Berguna untuk proxy, titik akhir khusus, atau konfigurasi model seluruh tim.

Panggilan yang dilakukan selama fungsi pabrik ekstensi dimasukkan ke dalam antrean dan diterapkan setelah pelari melakukan inisialisasi. Panggilan yang dilakukan setelah itu — misalnya dari pengendali perintah yang mengikuti alur pengaturan pengguna — langsung berlaku tanpa memerlukan /reload.

Penyedia dinamis dapat menerapkan refreshModels. Pi memanggilnya selama penyegaran model, menerbitkan daftar yang dikembalikan secara sinkron melalui penyedia, dan meneruskan konteks kredensial/katalog tersimpan/jaringan/sinyal kanonik. Ekstensi memutuskan apakah akan mempertahankan metadata katalog melalui pemeriksaan generasi context.publish({ persist: entry }); server langsung seperti llama.cpp dapat mengembalikan model tanpa menyimpannya.

context.signal selalu merupakan sinyal konkret dan callback penyedia harus meneruskannya ke pemblokiran I/O. Panggilan publik ModelRuntime.refresh() dan ModelRegistry.refresh() menerima sinyal opsional dan tidak dibatasi jika dihilangkan; ekstensi dan aplikasi memilih tenggat waktu mereka sendiri. Pembatalan menghentikan penelpon menunggu meskipun penyedia mengabaikan sinyalnya, namun kerja sama tetap diperlukan untuk menghentikan pekerjaan yang mendasarinya.

Extensions yang memerlukan autentikasi, pemfilteran, penyegaran, atau perilaku streaming penyedia asli dapat mendaftarkan Provider lengkap dari @earendil-works/pi-ai. Penyedia menjadi basis komposisi dan penggantian models.json masih berlaku di atasnya.

import { createProvider, openAICompletionsApi } from "@earendil-works/pi-ai";

const provider = createProvider({
  id: "local-server",
  name: "Local Server",
  baseUrl: "http://localhost:8080/v1",
  auth: {
    apiKey: {
      name: "Local server setup",
      async login(interaction) {
        return {
          type: "api_key",
          key: await interaction.prompt({ type: "secret", message: "API key" }),
        };
      },
      async resolve({ credential }) {
        return credential?.key
          ? { auth: { apiKey: credential.key }, source: "stored API key" }
          : undefined;
      },
    },
  },
  models: [],
  api: openAICompletionsApi(),
});

pi.registerProvider(provider);

// Register a new provider with custom models
pi.registerProvider("my-proxy", {
  name: "My Proxy",
  baseUrl: "https://proxy.example.com",
  apiKey: "$PROXY_API_KEY",  // env var reference
  api: "anthropic-messages",
  models: [
    {
      id: "claude-sonnet-4-20250514",
      name: "Claude 4 Sonnet (proxy)",
      reasoning: false,
      input: ["text", "image"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 200000,
      maxTokens: 16384
    }
  ]
});

// Register a live llama.cpp catalog without persisting discovered models
pi.registerProvider("llama.cpp", {
  baseUrl: "http://localhost:8080/v1",
  apiKey: "local",
  api: "openai-completions",
  async refreshModels({ signal }) {
    const response = await fetch("http://localhost:8080/v1/models", { signal });
    const { data } = await response.json();
    return data.map(({ id }) => ({
      id,
      name: id,
      reasoning: false,
      input: ["text"],
      cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
      contextWindow: 128000,
      maxTokens: 16384
    }));
  }
});

// Override baseUrl for an existing provider (keeps all models)
pi.registerProvider("anthropic", {
  baseUrl: "https://proxy.example.com"
});

// Register provider with OAuth support for /login
pi.registerProvider("corporate-ai", {
  baseUrl: "https://ai.corp.com",
  api: "openai-responses",
  models: [...],
  oauth: {
    name: "Corporate AI (SSO)",
    async login(callbacks) {
      // Custom OAuth flow
      callbacks.onAuth({ url: "https://sso.corp.com/..." });
      const code = await callbacks.onPrompt({ message: "Enter code:" });
      return { refresh: code, access: code, expires: Date.now() + 3600000 };
    },
    async refreshToken(credentials, signal) {
      signal.throwIfAborted();
      // Refresh logic
      return credentials;
    },
    getApiKey(credentials) {
      return credentials.access;
    }
  }
});

Bentuk objek menerima pi-ai lengkap Provider, termasuk perilaku asli auth, getModels, refreshModels, filterModels, stream, dan streamSimple.

Opsi konfigurasi lama:

  • name - Nama tampilan untuk penyedia di UI seperti /login.
  • baseUrl - API URL titik akhir. Diperlukan saat mendefinisikan model.
  • apiKey - API key literal, interpolasi lingkungan ($ENV_VAR atau ${ENV_VAR}), atau awalan !command. Diperlukan saat menentukan model (kecuali oauth disediakan). $ lolos dari ``apiKey - API key literal, interpolasi lingkungan ($ENV_VARatau${ENV_VAR}), atau awalan !command. Diperlukan saat menentukan model (kecuali oauthdisediakan).$lolos dari, dan$!lolos dari!` literal tanpa memicu eksekusi perintah.
  • api - API ketik: "anthropic-messages", "openai-completions", "openai-responses", dll.
  • headers - Header khusus untuk disertakan dalam permintaan.
  • authHeader - Jika benar, tambahkan header Authorization: Bearer secara otomatis.
  • models - Kumpulan definisi model. Jika tersedia, gantikan semua model yang ada untuk penyedia ini. Definisi model dapat mengatur baseUrl untuk mengganti titik akhir penyedia untuk model tersebut.
  • refreshModels - Panggilan balik penemuan dinamis asinkron. Model yang dikembalikan menggantikan model yang disediakan ekstensi. context.stored berisi snapshot penyedia yang ada; gunakan generasi-diperiksa context.publish({ persist: entry }) hanya ketika data katalog yang diperbarui harus tetap ada. Gunakan persist: null untuk menghapus snapshot itu.
  • oauth - OAuth konfigurasi penyedia untuk dukungan /login. Jika disediakan, penyedia muncul di menu login.
  • streamSimple - Implementasi streaming khusus untuk API non-standar.

Lihat custom-provider.md untuk topik lanjutan: streaming khusus APIs, detail OAuth, referensi definisi model.

pi.unregisterProvider(nama)

Hapus penyedia yang terdaftar sebelumnya dan modelnya. Model bawaan yang diganti oleh penyedia akan dipulihkan. Tidak berpengaruh jika penyedia tidak terdaftar.

Seperti registerProvider, ini berlaku segera ketika dipanggil setelah fase beban awal, jadi /reload tidak diperlukan.

pi.registerCommand("my-setup-teardown", {
  description: "Remove the custom proxy provider",
  handler: async (_args, _ctx) => {
    pi.unregisterProvider("my-proxy");
  },
});

Manajemen Negara

Extensions dengan negara harus menyimpannya dalam hasil alat details untuk dukungan percabangan yang tepat:

export default function (pi: ExtensionAPI) {
  let items: string[] = [];

  // Reconstruct state from session
  pi.on("session_start", async (_event, ctx) => {
    items = [];
    for (const entry of ctx.sessionManager.getBranch()) {
      if (entry.type === "message" && entry.message.role === "toolResult") {
        if (entry.message.toolName === "my_tool") {
          items = entry.message.details?.items ?? [];
        }
      }
    }
  });

  pi.registerTool({
    name: "my_tool",
    // ...
    async execute(toolCallId, params, signal, onUpdate, ctx) {
      items.push("new item");
      return {
        content: [{ type: "text", text: "Added" }],
        details: { items: [...items] },  // Store for reconstruction
      };
    },
  });
}

Alat Kustom

Daftarkan alat yang dapat dihubungi LLM melalui pi.registerTool(). Alat muncul di prompt sistem dan dapat memiliki rendering khusus.

Gunakan promptSnippet untuk entri satu baris pendek di bagian Available tools pada prompt sistem default. Jika dihilangkan, alat khusus tidak dimasukkan dalam bagian itu.

Gunakan promptGuidelines untuk menambahkan poin khusus alat ke bagian prompt sistem default Guidelines. Poin-poin ini hanya disertakan saat alat aktif (misalnya, setelah pi.setActiveTools([...])).

Penting: promptGuidelines poin ditambahkan rata ke bagian Guidelines tanpa awalan atau pengelompokan nama alat. Setiap pedoman harus menyebutkan alat yang dirujuknya — hindari "Gunakan alat ini ketika..." karena LLM tidak dapat membedakan alat mana yang dimaksud dengan "ini". Tulis "Gunakan my_tool ketika..." sebagai gantinya.

Catatan: Beberapa model bodoh dan menyertakan awalan @ dalam argumen jalur alat. Alat bawaan menghapus @ terdepan sebelum menyelesaikan jalur. Jika alat khusus Anda menerima jalur, normalkan juga @ di depannya.

Jika alat khusus Anda memutasi file, gunakan withFileMutationQueue() sehingga alat tersebut berpartisipasi dalam antrean per file yang sama dengan edit dan write bawaan. Hal ini penting karena pemanggilan alat dijalankan secara paralel secara default. Tanpa antrian, dua alat dapat membaca konten file lama yang sama, menghitung pembaruan yang berbeda, dan kemudian penulisan mana pun yang terakhir akan menimpa yang lain.

Contoh kasus kegagalan: alat khusus Anda mengedit foo.ts sementara edit bawaan juga mengubah foo.ts pada giliran asisten yang sama. Jika alat Anda tidak berpartisipasi dalam antrean, keduanya dapat membaca foo.ts asli, menerapkan perubahan terpisah, dan salah satu perubahan tersebut akan hilang.

Teruskan jalur file target sebenarnya ke withFileMutationQueue(), bukan argumen pengguna mentah. Selesaikan terlebih dahulu ke jalur absolut, relatif terhadap ctx.cwd atau direktori kerja alat Anda. Untuk file yang sudah ada, helper melakukan kanonikalisasi melalui realpath(), jadi alias symlink untuk file yang sama berbagi satu antrian. Untuk file baru, file tersebut kembali ke jalur absolut yang diselesaikan karena belum ada apa pun untuk realpath().

Antrian seluruh jendela mutasi pada jalur target itu. Itu termasuk logika baca-modifikasi-tulis, bukan hanya penulisan akhir.

import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";

async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
  const absolutePath = resolve(ctx.cwd, params.path);

  return withFileMutationQueue(absolutePath, async () => {
    await mkdir(dirname(absolutePath), { recursive: true });
    const current = await readFile(absolutePath, "utf8");
    const next = current.replace(params.oldText, params.newText);
    await writeFile(absolutePath, next, "utf8");

    return {
      content: [{ type: "text", text: `Updated ${params.path}` }],
      details: {},
    };
  });
}

Definisi Alat

import { Type } from "typebox";
import { StringEnum } from "@earendil-works/pi-ai";
import { Text } from "@earendil-works/pi-tui";

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "What this tool does (shown to LLM)",
  promptSnippet: "List or add items in the project todo list",
  promptGuidelines: [
    "Use my_tool for todo planning instead of direct file edits when the user asks for a task list."
  ],
  parameters: Type.Object({
    action: StringEnum(["list", "add"] as const),  // Use StringEnum for Google compatibility
    text: Type.Optional(Type.String()),
  }),
  prepareArguments(args) {
    if (!args || typeof args !== "object") return args;
    const input = args as { action?: string; oldAction?: string };
    if (typeof input.oldAction === "string" && input.action === undefined) {
      return { ...input, action: input.oldAction };
    }
    return args;
  },

  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // Check for cancellation
    if (signal?.aborted) {
      return { content: [{ type: "text", text: "Cancelled" }] };
    }

    // Stream progress updates
    onUpdate?.({
      content: [{ type: "text", text: "Working..." }],
      details: { progress: 50 },
    });

    // Run commands via pi.exec (captured from extension closure)
    const result = await pi.exec("some-command", [], { signal });

    // Return result
    return {
      content: [{ type: "text", text: "Done" }],  // Sent to LLM
      details: { data: result },                   // For rendering & state
      // usage: nestedModelResponse.usage,          // Optional nested LLM usage
      // Optional: stop after this tool batch when every finalized tool result
      // in the batch also returns terminate: true.
      terminate: true,
    };
  },

  // Optional: Custom rendering
  renderCall(args, theme, context) { ... },
  renderResult(result, options, theme, context) { ... },
});

Penghitungan penggunaan: Jika alat melakukan panggilan LLM bertingkat, kembalikan gabungan Usage sebagai usage. Pi menyimpannya pada hasil alat dan memasukkannya ke dalam footer, /session, dan RPC total sesi. tool_result penangan dapat memeriksa atau mengganti nilai ini.

Kesalahan sinyal: Untuk menandai eksekusi alat sebagai gagal (menetapkan isError: true pada hasil dan melaporkannya ke LLM), memunculkan kesalahan dari execute. Mengembalikan nilai tidak pernah menyetel tanda kesalahan apa pun properti yang Anda sertakan dalam objek pengembalian.

Penghentian awal: Kembalikan terminate: true dari execute() untuk memberi petunjuk bahwa panggilan LLM tindak lanjut otomatis harus dilewati setelah kumpulan alat saat ini. Ini hanya berlaku ketika setiap hasil alat yang diselesaikan dalam batch tersebut dihentikan. Lihat examples/extensions/structured-output.ts untuk contoh minimal saat agen mengakhiri panggilan alat keluaran terstruktur akhir.

// Correct: throw to signal an error
async execute(toolCallId, params) {
  if (!isValid(params.input)) {
    throw new Error(`Invalid input: ${params.input}`);
  }
  return { content: [{ type: "text", text: "OK" }], details: {} };
}

Penting: Gunakan StringEnum dari @earendil-works/pi-ai untuk enum string. Type.Union/Type.Literal tidak berfungsi dengan API Google.

Persiapan argumen: prepareArguments(args) bersifat opsional. Jika ditentukan, ini berjalan sebelum validasi skema dan sebelum execute(). Gunakan ini untuk meniru bentuk input lama yang diterima ketika pi melanjutkan sesi lama yang argumen pemanggilan alatnya tidak lagi cocok dengan skema saat ini. Kembalikan objek yang ingin Anda validasi terhadap parameters. Jaga agar skema publik tetap ketat. Jangan menambahkan bidang kompatibilitas yang tidak digunakan lagi ke parameters hanya agar sesi lama yang dilanjutkan tetap berfungsi.

Contoh: sesi lama mungkin berisi panggilan alat edit dengan oldText dan newText tingkat atas, sedangkan skema saat ini hanya menerima edits: [{ oldText, newText }].

pi.registerTool({
  name: "edit",
  label: "Edit",
  description: "Edit a single file using exact text replacement",
  parameters: Type.Object({
    path: Type.String(),
    edits: Type.Array(
      Type.Object({
        oldText: Type.String(),
        newText: Type.String(),
      }),
    ),
  }),
  prepareArguments(args) {
    if (!args || typeof args !== "object") return args;

    const input = args as {
      path?: string;
      edits?: Array<{ oldText: string; newText: string }>;
      oldText?: unknown;
      newText?: unknown;
    };

    if (typeof input.oldText !== "string" || typeof input.newText !== "string") {
      return args;
    }

    return {
      ...input,
      edits: [...(input.edits ?? []), { oldText: input.oldText, newText: input.newText }],
    };
  },
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    // params now matches the current schema
    return {
      content: [{ type: "text", text: `Applying ${params.edits.length} edit block(s)` }],
      details: {},
    };
  },
});

Mengganti Alat Bawaan

Extensions dapat mengganti alat bawaan (read, bash, edit, write, grep, find, ls) dengan mendaftarkan alat dengan nama yang sama. Mode interaktif menampilkan peringatan ketika hal ini terjadi.

# Extension's read tool replaces built-in read
pi -e ./tool-override.ts

Alternatifnya, gunakan --no-builtin-tools untuk memulai tanpa alat bawaan apa pun sambil tetap mengaktifkan alat ekstensi:

# No built-in tools, only extension tools
pi --no-builtin-tools -e ./my-extension.ts

Lihat examples/extensions/tool-override.ts untuk contoh lengkap yang menggantikan read dengan logging dan kontrol akses.

Rendering: Warisan perender bawaan diselesaikan per slot. Penimpaan eksekusi dan pengesampingan rendering bersifat independen. Jika penggantian Anda menghilangkan renderCall, renderCall bawaan akan digunakan. Jika penggantian Anda menghilangkan renderResult, renderResult bawaan akan digunakan. Jika penggantian Anda menghilangkan keduanya, penyaji bawaan akan digunakan secara otomatis (penyorotan sintaksis, perbedaan, dll.). Hal ini memungkinkan Anda menggabungkan alat bawaan untuk logging atau kontrol akses tanpa mengimplementasikan ulang UI.

Metadata cepat: promptSnippet dan promptGuidelines tidak diwarisi dari alat bawaan. Jika penggantian Anda harus menyimpan instruksi cepat tersebut, tentukan instruksi tersebut pada penggantian secara eksplisit.

Penerapan Anda harus sesuai dengan bentuk hasil yang tepat, termasuk jenis details. Logika UI dan sesi bergantung pada bentuk ini untuk rendering dan pelacakan status.

Implementasi alat bawaan:

Eksekusi Jarak Jauh

Alat bawaan mendukung operasi yang dapat dicolokkan untuk mendelegasikan ke sistem jarak jauh (SSH, container, dll.):

import { createReadTool, createBashTool, type ReadOperations } from "@earendil-works/pi-coding-agent";

// Create tool with custom operations
const remoteRead = createReadTool(cwd, {
  operations: {
    readFile: (path) => sshExec(remote, `cat ${path}`),
    access: (path) => sshExec(remote, `test -r ${path}`).then(() => {}),
  }
});

// Register, checking flag at execution time
pi.registerTool({
  ...remoteRead,
  async execute(id, params, signal, onUpdate, _ctx) {
    const ssh = getSshConfig();
    if (ssh) {
      const tool = createReadTool(cwd, { operations: createRemoteOps(ssh) });
      return tool.execute(id, params, signal, onUpdate);
    }
    return localRead.execute(id, params, signal, onUpdate);
  },
});

Antarmuka operasi: ReadOperations, WriteOperations, EditOperations, BashOperations, LsOperations, GrepOperations, FindOperations

Untuk user_bash, ekstensi dapat menggunakan kembali backend shell lokal pi melalui createLocalBashOperations() alih-alih mengimplementasikan ulang pemijahan proses lokal, resolusi shell, dan penghentian pohon proses.

Alat bash juga mendukung spawn hook untuk menyesuaikan perintah, cwd, atau env sebelum eksekusi:

import { createBashTool } from "@earendil-works/pi-coding-agent";

const bashTool = createBashTool(cwd, {
  spawnHook: ({ command, cwd, env }) => ({
    command: `source ~/.profile\n${command}`,
    cwd: `/mnt/sandbox${cwd}`,
    env: { ...env, CI: "1" },
  }),
});

createBashTool() memaparkan sesi saat ini ke perintah melalui PI_SESSION_ID, PI_SESSION_FILE, PI_PROVIDER, PI_MODEL, dan PI_REASONING_LEVEL. Injeksi terjadi sebelum spawnHook, jadi hook menerima nilai ini di env dan mempertahankannya saat menyebarkan lingkungan yang ada seperti di atas. Atur exposeSessionEnvironment: false untuk menonaktifkannya:

const bashTool = createBashTool(cwd, {
  exposeSessionEnvironment: false,
});

Lihat Bash tool session environment untuk semantik variabel. Lihat examples/extensions/ssh.ts untuk contoh SSH lengkap dengan tanda --ssh.

Pemotongan Keluaran

Alat HARUS memotong keluarannya untuk menghindari konteks LLM yang berlebihan. Output yang besar dapat menyebabkan:

  • Kesalahan luapan konteks (prompt terlalu panjang)
  • Kegagalan pemadatan
  • Performa model menurun

Batas bawaannya adalah 50KB (~10 ribu token) dan 2000 baris, mana saja yang tercapai terlebih dahulu. Gunakan utilitas pemotongan yang diekspor:

import {
  truncateHead,      // Keep first N lines/bytes (good for file reads, search results)
  truncateTail,      // Keep last N lines/bytes (good for logs, command output)
  truncateLine,      // Truncate a single line to maxBytes with ellipsis
  formatSize,        // Human-readable size (e.g., "50KB", "1.5MB")
  DEFAULT_MAX_BYTES, // 50KB
  DEFAULT_MAX_LINES, // 2000
} from "@earendil-works/pi-coding-agent";

async execute(toolCallId, params, signal, onUpdate, ctx) {
  const output = await runCommand();

  // Apply truncation
  const truncation = truncateHead(output, {
    maxLines: DEFAULT_MAX_LINES,
    maxBytes: DEFAULT_MAX_BYTES,
  });

  let result = truncation.content;

  if (truncation.truncated) {
    // Write full output to temp file
    const tempFile = writeTempFile(output);

    // Inform the LLM where to find complete output
    result += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
    result += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).`;
    result += ` Full output saved to: ${tempFile}]`;
  }

  return { content: [{ type: "text", text: result }] };
}

Poin-poin penting:

  • Gunakan truncateHead untuk konten yang bagian awalnya penting (hasil pencarian, pembacaan file)
  • Gunakan truncateTail untuk konten yang ujungnya penting (log, keluaran perintah)
  • Selalu beri tahu LLM ketika keluaran terpotong dan di mana menemukan versi lengkapnya
  • Dokumentasikan batas pemotongan dalam deskripsi alat Anda

Lihat examples/extensions/truncated-tool.ts untuk contoh lengkap membungkus rg (ripgrep) dengan pemotongan yang tepat.

Berbagai Alat

Satu ekstensi dapat mendaftarkan beberapa alat dengan status bersama:

export default function (pi: ExtensionAPI) {
  let connection = null;

  pi.registerTool({ name: "db_connect", ... });
  pi.registerTool({ name: "db_query", ... });
  pi.registerTool({ name: "db_close", ... });

  pi.on("session_shutdown", async () => {
    connection?.close();
  });
}

Rendering Kustom

Alat dapat menyediakan renderCall dan renderResult untuk tampilan TUI khusus. Lihat tui.md untuk komponen lengkap API dan tool-execution.ts untuk mengetahui bagaimana baris alat disusun.

Secara default, keluaran alat dibungkus dengan Box yang menangani padding dan latar belakang. renderCall atau renderResult yang ditentukan harus menghasilkan Component. Jika penyaji slot tidak ditentukan, tool-execution.ts menggunakan rendering cadangan untuk slot tersebut.

Setel renderShell: "self" kapan alat harus merender shellnya sendiri alih-alih menggunakan Box default. Hal ini berguna untuk alat yang memerlukan kontrol penuh atas perilaku pembingkaian atau latar belakang, misalnya pratinjau besar yang harus tetap stabil secara visual setelah alat tersebut dipasang.

pi.registerTool({
  name: "my_tool",
  label: "My Tool",
  description: "Custom shell example",
  parameters: Type.Object({}),
  renderShell: "self",
  async execute() {
    return { content: [{ type: "text", text: "ok" }], details: undefined };
  },
  renderCall(args, theme, context) {
    return new Text(theme.fg("accent", "my custom shell"), 0, 0);
  },
});

renderCall dan renderResult masing-masing menerima objek context dengan:

  • args - argumen pemanggilan alat saat ini
  • state - status baris-lokal bersama di renderCall dan renderResult
  • lastComponent - komponen yang dikembalikan sebelumnya untuk slot tersebut, jika ada
  • invalidate() - meminta rendering baris alat ini
  • toolCallId, cwd, executionStarted, argsComplete, isPartial, expanded, showImages, isError

Gunakan context.state untuk status bersama lintas slot. Simpan cache slot-lokal pada instance komponen yang dikembalikan ketika Anda ingin menggunakan kembali dan mengubah komponen yang sama di seluruh render.

panggilan render

Merender panggilan alat atau header:

import { Text } from "@earendil-works/pi-tui";

renderCall(args, theme, context) {
  const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
  let content = theme.fg("toolTitle", theme.bold("my_tool "));
  content += theme.fg("muted", args.action);
  if (args.text) {
    content += " " + theme.fg("dim", `"${args.text}"`);
  }
  text.setText(content);
  return text;
}

hasil render

Merender hasil atau keluaran alat:

renderResult(result, { expanded, isPartial }, theme, context) {
  if (isPartial) {
    return new Text(theme.fg("warning", "Processing..."), 0, 0);
  }

  if (result.details?.error) {
    return new Text(theme.fg("error", `Error: ${result.details.error}`), 0, 0);
  }

  let text = theme.fg("success", "✓ Done");
  if (expanded && result.details?.items) {
    for (const item of result.details.items) {
      text += "\n  " + theme.fg("dim", item);
    }
  }
  return new Text(text, 0, 0);
}

Jika slot sengaja tidak memiliki konten yang terlihat, kembalikan Component kosong seperti Container kosong.

Petunjuk Pengikatan Kunci

Gunakan keyHint() untuk menampilkan petunjuk pengikatan kunci yang mengikuti konfigurasi pengikatan kunci aktif:

import { keyHint } from "@earendil-works/pi-coding-agent";

renderResult(result, { expanded }, theme, context) {
  let text = theme.fg("success", "✓ Done");
  if (!expanded) {
    text += ` (${keyHint("app.tools.expand", "to expand")})`;
  }
  return new Text(text, 0, 0);
}

Fungsi yang tersedia:

  • keyHint(keybinding, description) - Memformat id pengikat kunci yang dikonfigurasi seperti "app.tools.expand" atau "tui.select.confirm"
  • keyText(keybinding) - Mengembalikan teks kunci mentah yang dikonfigurasi untuk id pengikat kunci
  • rawKeyHint(key, description) - Memformat string kunci mentah

Gunakan id pengikat kunci dengan spasi nama:

  • Id agen pengkodean menggunakan namespace app.*, misalnya app.tools.expand, app.editor.external, app.session.rename
  • ID TUI yang dibagikan menggunakan namespace tui.*, misalnya tui.select.confirm, tui.select.cancel, tui.input.tab

Untuk daftar lengkap id dan default pengikat kunci, lihat keybindings.md. keybindings.json menggunakan id dengan namespace yang sama.

Editor khusus dan komponen ctx.ui.custom() menerima keybindings: KeybindingsManager sebagai argumen yang dimasukkan. Mereka harus menggunakan manajer yang disuntikkan itu secara langsung daripada menelepon getKeybindings() atau setKeybindings().

Praktik Terbaik

  • Gunakan Text dengan bantalan (0, 0). Kotak default menangani padding.
  • Gunakan \n untuk konten multi-baris.
  • Tangani isPartial untuk kemajuan streaming.
  • Dukungan expanded untuk detail sesuai permintaan.
  • Pertahankan tampilan default tetap ringkas.
  • Baca context.args di renderResult alih-alih menyalin argumen ke context.state.
  • Gunakan context.state hanya untuk data yang harus dibagikan ke seluruh slot panggilan dan hasil.
  • Gunakan kembali context.lastComponent ketika instance komponen yang sama dapat diperbarui di tempatnya.
  • Gunakan renderShell: "self" hanya ketika shell kotak default menghalangi. Dalam mode self-shell, alat ini bertanggung jawab atas framing, padding, dan latar belakangnya sendiri.

Penggantian

Jika penyaji slot tidak ditentukan atau muncul:

  • renderCall: Menampilkan nama alat
  • renderResult: Menampilkan teks mentah dari content

Pemuatan Alat Dinamis

Extensions dapat mendaftarkan banyak alat sambil tetap mengaktifkan set awal kecil. Sebuah alat kemudian dapat menambahkan lebih banyak alat dengan pi.setActiveTools() selama eksekusi. Pi mendeteksi perubahan aditif murni, mencatat nama alat yang baru tersedia pada hasil alat tersebut, dan menerapkan set aktif yang diperbarui sebelum permintaan model berikutnya.

Ini berfungsi pada setiap model. Models dengan dukungan pemuatan tertunda asli mempertahankan awalan prompt stabil dan memuat definisi baru pada posisi hasil pahat. Model lain menggunakan fallback yang dijelaskan di bawah.

Siklus hidupnya adalah:

  1. Daftarkan setiap alat dengan pi.registerTool() sehingga muncul di pi.getAllTools().
  2. Biarkan alat pemuat, seperti search_tools, tetap aktif dan biarkan alat yang dapat dicari tidak aktif.
  3. Selama eksekusi loader, panggil pi.setActiveTools([...currentTools,...matchingTools]). Perubahannya harus bersifat tambahan: jangan menghapus alat yang sedang aktif dalam panggilan yang sama.
  4. Pi mencatat alat mana yang ditambahkan pada hasil alat pemuat.
  5. Sebelum respons model berikutnya, Pi memaparkan definisi tambahan menggunakan pemuatan asli yang ditangguhkan jika didukung, atau daftar alat aktif normal sebaliknya.

Anda tidak perlu mengembalikan referensi alat khusus penyedia atau menandai pemuat sebagai alat pencarian khusus. Perubahan alat aktif adalah sinyalnya. Nama yang diteruskan ke pi.setActiveTools() harus sudah terdaftar; nama yang tidak diketahui diabaikan.

Models dengan pemuatan tertunda asli

  • Antropik
    • Models: Soneta, Opus, Fable versi 4.5 atau lebih baru (tanpa Haiku)
    • Representasi asli: Definisi yang ditangguhkan menggunakan defer_loading; titik muat menggunakan konten tool_reference.
  • BukaAI
    • Models: gpt-5.4 dan keluarga baru
    • Representasi asli: Pi menambahkan item klien tool_search_call dan tool_search_output yang telah selesai pada titik pemuatan.

Untuk model atau proksi khusus yang terverifikasi, penanganan asli dapat diaktifkan dengan compat.supportsToolReferences: true untuk anthropic-messages, atau compat.supportsToolSearch: true untuk openai-responses dan openai-codex-responses. Biarkan ini dinonaktifkan kecuali titik akhir dan model menerima protokol asli yang sesuai.

Perilaku mundur

Untuk semua model dan penyedia lainnya, aktivasi dinamis masih berfungsi: Pi mengirimkan daftar lengkap alat aktif saat ini secara normal pada permintaan berikutnya. Model dapat memanggil alat yang baru diaktifkan, namun menambahkan definisinya dapat membuat awalan prompt cache penyedia menjadi tidak valid.

Pi juga menggunakan fallback aman ini ketika set aktif tidak murni bersifat aditif, seperti mengganti satu kelompok alat dengan kelompok alat lainnya. Oleh karena itu, penghapusan alat dapat dilakukan, tetapi tidak menggunakan pemuatan yang ditangguhkan.

Untuk perilaku cache terbaik, biarkan alat pemuat tetap aktif sepanjang sesi dan tambahkan alat alih-alih mengganti set yang aktif. Perhatikan juga bahwa mengaktifkan alat dengan promptSnippet atau promptGuidelines akan membangun kembali prompt sistem; perubahan yang dilakukan segera oleh sistem dapat membatalkan awalan meskipun penyedia mendukung skema yang ditangguhkan. Alat yang dimuat dengan lambat biasanya harus mengandalkan alatnya description dan menghilangkan metadata prompt yang hanya aktif.

Contoh alat pencarian

Ekstensi berikut mendaftarkan dua alat yang dapat dicari, menghapusnya dari set aktif awal, dan hanya menyimpan search_tools sebagai pemuatnya. Contohnya menggunakan pencocokan kata kunci sederhana, namun penerapan penelusuran dapat menggunakan BM25, penyematan, katalog jarak jauh, atau perutean khusus proyek.

import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";

const SEARCHABLE_TOOL_NAMES = new Set(["lookup_weather", "search_issues"]);

export default function (pi: ExtensionAPI) {
  pi.registerTool({
    name: "lookup_weather",
    label: "Lookup Weather",
    description: "Look up the current weather for a city",
    parameters: Type.Object({ city: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `Weather for ${params.city}: sunny` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_issues",
    label: "Search Issues",
    description: "Search project issues by keyword",
    parameters: Type.Object({ query: Type.String() }),
    async execute(_toolCallId, params) {
      return {
        content: [{ type: "text", text: `No open issues matching ${params.query}` }],
        details: {},
      };
    },
  });

  pi.registerTool({
    name: "search_tools",
    label: "Search Tools",
    description: "Search for and enable tools relevant to a task",
    promptSnippet: "Search for additional tools when the active tools cannot perform the task",
    promptGuidelines: [
      "Use search_tools when a task requires a capability that is not currently available.",
    ],
    parameters: Type.Object({
      query: Type.String({ description: "Capability or task to search for" }),
      limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
    }),
    async execute(_toolCallId, params) {
      const terms = params.query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
      const matches = pi.getAllTools()
        .filter((tool) => SEARCHABLE_TOOL_NAMES.has(tool.name))
        .map((tool) => ({
          tool,
          score: terms.reduce(
            (score, term) =>
              score + (`${tool.name} ${tool.description}`.toLowerCase().includes(term) ? 1 : 0),
            0,
          ),
        }))
        .filter((match) => match.score > 0)
        .sort((a, b) => b.score - a.score)
        .slice(0, params.limit ?? 3)
        .map((match) => match.tool.name);

      if (matches.length === 0) {
        return {
          content: [{ type: "text", text: `No tools found for: ${params.query}` }],
          details: { matches: [] },
        };
      }

      const active = pi.getActiveTools();
      const added = matches.filter((name) => !active.includes(name));
      pi.setActiveTools([...new Set([...active, ...added])]);

      return {
        content: [{
          type: "text",
          text: added.length > 0
            ? `Loaded tools: ${added.join(", ")}`
            : `Matching tools already active: ${matches.join(", ")}`,
        }],
        details: { matches, added },
      };
    },
  });

  pi.on("session_start", () => {
    // Keep searchable tools registered but initially inactive. Preserve built-ins
    // and tools owned by other extensions, and keep the loader itself active.
    const initialTools = pi.getActiveTools().filter(
      (name) => !SEARCHABLE_TOOL_NAMES.has(name),
    );
    pi.setActiveTools([...new Set([...initialTools, "search_tools"])]);
  });
}

Saat search_tools menambahkan kecocokan, model menerima definisi tersebut segera setelah permintaan. Pada model berkemampuan asli, definisi tersebut ditetapkan setelah hasil pencarian tanpa mengubah awalan skema alat awal. Pada model lain, alat ini muncul dalam daftar alat normal berdasarkan permintaan berikut yang sama.

UI khusus

Extensions dapat berinteraksi dengan pengguna melalui metode ctx.ui dan menyesuaikan cara pesan/alat ditampilkan.

Untuk komponen khusus, lihat tui.md yang memiliki pola salin-tempel untuk:

  • Dialog pemilihan (SelectList)
  • Operasi asinkron dengan pembatalan (BorderedLoader)
  • Pengalih pengaturan (Daftar Pengaturan)
  • Indikator status (setStatus)
  • Pesan, visibilitas, dan indikator yang berfungsi selama streaming (setWorkingMessage, setWorkingVisible, setWorkingIndicator)
  • Widget di atas/di bawah editor (setWidget)
  • Penyedia pelengkapan otomatis yang berlapis di atas garis miring/penyelesaian jalur bawaan (addAutocompleteProvider)
  • Footer khusus (setFooter)

Dialog

// Select from options
const choice = await ctx.ui.select("Pick one:", ["A", "B", "C"]);

// Confirm dialog
const ok = await ctx.ui.confirm("Delete?", "This cannot be undone");

// Text input
const name = await ctx.ui.input("Name:", "placeholder");

// Multi-line editor
const text = await ctx.ui.editor("Edit:", "prefilled text");

// Notification (non-blocking)
ctx.ui.notify("Done!", "info");  // "info" | "warning" | "error"

Dialog Jangka Waktu dengan Hitung Mundur

Dialog mendukung opsi timeout yang ditutup secara otomatis dengan tampilan hitung mundur langsung:

// Dialog shows "Title (5s)" → "Title (4s)" → ... → auto-dismisses at 0
const confirmed = await ctx.ui.confirm(
  "Timed Confirmation",
  "This dialog will auto-cancel in 5 seconds. Confirm?",
  { timeout: 5000 }
);

if (confirmed) {
  // User confirmed
} else {
  // User cancelled or timed out
}

Nilai pengembalian saat batas waktu habis:

  • select() mengembalikan undefined
  • confirm() mengembalikan false
  • input() mengembalikan undefined

Pemberhentian Manual dengan AbortSignal

Untuk kontrol lebih lanjut (misalnya, untuk membedakan batas waktu dari pembatalan pengguna), gunakan AbortSignal:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);

const confirmed = await ctx.ui.confirm(
  "Timed Confirmation",
  "This dialog will auto-cancel in 5 seconds. Confirm?",
  { signal: controller.signal }
);

clearTimeout(timeoutId);

if (confirmed) {
  // User confirmed
} else if (controller.signal.aborted) {
  // Dialog timed out
} else {
  // User cancelled (pressed Escape or selected "No")
}

Lihat examples/extensions/timed-confirm.ts untuk contoh lengkap.

// 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

Bingkai indikator kerja khusus ditampilkan secara verbatim. Jika Anda menginginkan warna, tambahkan sendiri warna tersebut ke string bingkai, misalnya dengan ctx.ui.theme.fg(...).

Pelengkapan otomatis Providers

Gunakan ctx.ui.addAutocompleteProvider() untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. Setel triggerCharacters untuk pemicu alami khusus seperti Gunakan ctx.ui.addAutocompleteProvider()untuk menumpuk logika pelengkapan otomatis khusus di atas perintah garis miring dan penyedia jalur bawaan. SeteltriggerCharacters` untuk pemicu alami khusus seperti.

Pola khas:

  • periksa teks sebelum kursor
  • kembalikan saran Anda sendiri ketika sintaks khusus ekstensi Anda cocok
  • jika tidak, delegasikan ke current.getSuggestions(...)
  • delegasikan applyCompletion(...) kecuali Anda memerlukan perilaku penyisipan khusus
pi.on("session_start", (_event, ctx) => {
  ctx.ui.addAutocompleteProvider((current) => ({
    triggerCharacters: ["#"],
    async getSuggestions(lines, cursorLine, cursorCol, options) {
      const line = lines[cursorLine] ?? "";
      const beforeCursor = line.slice(0, cursorCol);
      const match = beforeCursor.match(/(?:^|[ \t])#([^\s#]*)$/);
      if (!match) {
        return current.getSuggestions(lines, cursorLine, cursorCol, options);
      }

      return {
        prefix: `#${match[1] ?? ""}`,
        items: [
          { value: "#2983", label: "#2983", description: "Extension API for registering custom @ autocomplete providers" },
          { value: "#2753", label: "#2753", description: "Reload stale resource settings" },
        ],
      };
    },

    applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
      return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
    },

    shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
      return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
    },
  }));
});

Lihat github-issue-autocomplete.ts untuk contoh lengkap yang memuat masalah GitHub terbuka terbaru dengan gh issue list dan memfilternya secara lokal untuk penyelesaian #... yang cepat. Ini memerlukan GitHub CLI (gh) dan checkout repositori GitHub.

Komponen Khusus

Untuk UI yang kompleks, gunakan ctx.ui.custom(). Ini untuk sementara menggantikan editor dengan komponen Anda hingga done() dipanggil:

import { Text, Component } from "@earendil-works/pi-tui";

const result = await ctx.ui.custom<boolean>((tui, theme, keybindings, done) => {
  const text = new Text("Press Enter to confirm, Escape to cancel", 1, 1);

  text.onKey = (key) => {
    if (key === "return") done(true);
    if (key === "escape") done(false);
    return true;
  };

  return text;
});

if (result) {
  // User pressed Enter
}

Panggilan balik menerima:

  • tui - TUI contoh (untuk dimensi layar, manajemen fokus)
  • theme - Tema penataan gaya saat ini
  • keybindings - Manajer pengikat tombol aplikasi (untuk memeriksa pintasan)
  • done(value) - Panggilan untuk menutup komponen dan mengembalikan nilai

Lihat tui.md untuk komponen lengkap API.

Mode Hamparan (Eksperimental)

Teruskan { overlay: true } untuk merender komponen sebagai modal mengambang di atas konten yang sudah ada, tanpa mengosongkan layar:

const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
  { overlay: true }
);

Untuk pemosisian lanjutan (jangkar, margin, persentase, visibilitas responsif), teruskan overlayOptions. Gunakan onHandle untuk mengontrol fokus atau visibilitas secara terprogram:

const result = await ctx.ui.custom<string | null>(
  (tui, theme, keybindings, done) => new MyOverlayComponent({ onClose: done }),
  {
    overlay: true,
    overlayOptions: { anchor: "top-right", width: "50%", margin: 2 },
    onHandle: (handle) => {
      handle.focus(); // focus this overlay and bring it to the visual front
      // handle.unfocus({ target: editorComponent }); // release input to a specific component
      // handle.setHidden(true/false); // toggle visibility
      // handle.hide(); // permanently remove
    }
  }
);

Hamparan terlihat terfokus dapat memperoleh kembali masukan setelah UI khusus non-hamparan sementara ditutup. Jika Anda sengaja ingin komponen lain tetap memasukkan input sementara overlay tetap terlihat, panggil handle.unfocus({ target }). Melewati { target: null } akan melepaskan overlay tanpa memfokuskan komponen lain.

Lihat tui.md untuk OverlayOptions lengkap dan OverlayHandle API dan overlay-qa-tests.ts sebagai contoh.

Editor Kustom

Ganti editor masukan utama dengan implementasi khusus (mode vim, mode emacs, dll.):

import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { matchesKey } from "@earendil-works/pi-tui";

class VimEditor extends CustomEditor {
  private mode: "normal" | "insert" = "insert";

  handleInput(data: string): void {
    if (matchesKey(data, "escape") && this.mode === "insert") {
      this.mode = "normal";
      return;
    }
    if (this.mode === "normal" && data === "i") {
      this.mode = "insert";
      return;
    }
    super.handleInput(data);  // App keybindings + text editing
  }
}

export default function (pi: ExtensionAPI) {
  pi.on("session_start", (_event, ctx) => {
    ctx.ui.setEditorComponent((tui, theme, keybindings) =>
      new VimEditor(tui, theme, keybindings)
    );
  });
}

Poin-poin penting:

  • Perpanjang CustomEditor (bukan basis Editor) untuk mendapatkan ikatan kunci aplikasi (escape untuk membatalkan, ctrl+d, peralihan model)
  • Hubungi super.handleInput(data) untuk kunci yang tidak Anda tangani
  • Pabrik menerima tui, theme, dan keybindings dari aplikasi
  • Gunakan ctx.ui.getEditorComponent() sebelum setEditorComponent() untuk menggabungkan editor khusus yang telah dikonfigurasi sebelumnya
  • Lewati undefined untuk mengembalikan default: ctx.ui.setEditorComponent(undefined)

Untuk menulis dengan ekstensi lain yang telah menggantikan editor, ambil pabrik sebelumnya sebelum mengatur milik Anda:

const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
  new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);

Lihat tui.md Pola 7 untuk contoh lengkap dengan indikator mode.

Rendering Pesan dan Entri

Daftarkan penyaji khusus untuk pesan dengan customType Anda. Gunakan penyaji pesan untuk konten yang harus berpartisipasi dalam konteks LLM:

import { Text } from "@earendil-works/pi-tui";

pi.registerMessageRenderer("my-extension", (message, options, theme) => {
  const { expanded, outputPad } = options;
  let text = theme.fg("accent", `[${message.customType}] `);
  text += message.content;

  if (expanded && message.details) {
    text += "\n" + theme.fg("dim", JSON.stringify(message.details, null, 2));
  }

  return new Text(text, outputPad, 0);
});

Pesan dikirim melalui pi.sendMessage():

pi.sendMessage({
  customType: "my-extension",  // Matches registerMessageRenderer
  content: "Status update",
  display: true,               // Show in TUI
  details: { ... },            // Available in renderer
});

Untuk konten khusus TUI yang tidak boleh dikirim ke LLM, render entri khusus sebagai gantinya:

pi.registerEntryRenderer("my-card", (entry, options, theme) => {
  return new Text(theme.fg("accent", JSON.stringify(entry.data)));
});

pi.appendEntry("my-card", { status: "done" });

Warna Tema

Semua fungsi render menerima objek theme. Lihat themes.md untuk membuat tema khusus dan palet warna lengkap.

// Foreground colors
theme.fg("toolTitle", text)   // Tool names
theme.fg("accent", text)      // Highlights
theme.fg("success", text)     // Success (green)
theme.fg("error", text)       // Errors (red)
theme.fg("warning", text)     // Warnings (yellow)
theme.fg("muted", text)       // Secondary text
theme.fg("dim", text)         // Tertiary text

// Text styles
theme.bold(text)
theme.italic(text)
theme.strikethrough(text)

Untuk penyorotan sintaksis pada perender alat khusus:

import { highlightCode, getLanguageFromPath } from "@earendil-works/pi-coding-agent";

// Highlight code with explicit language
const highlighted = highlightCode("const x = 1;", "typescript", theme);

// Auto-detect language from file path
const lang = getLanguageFromPath("/path/to/file.rs");  // "rust"
const highlighted = highlightCode(code, lang, theme);

Penanganan Kesalahan

  • Kesalahan ekstensi dicatat, agen melanjutkan
  • tool_call kesalahan memblokir alat (aman dari kegagalan)
  • Kesalahan alat execute harus ditandai dengan melempar; kesalahan yang terjadi ditangkap, dilaporkan ke LLM dengan isError: true, dan eksekusi dilanjutkan

Modus Perilaku

Mode ctx.mode ctx.hasUI Catatan
Interaktif "tui" true Penuh TUI dengan rendering terminal
RPC (--mode rpc) "rpc" true Dialog dan pemberitahuan melalui protokol JSON; custom() mengembalikan undefined. Lihat rpc.md
JSON (--mode json) "json" false Aliran acara ke stdout; Metode UI tidak boleh dilakukan
Cetak (-p) "print" false Extensions dijalankan tetapi tidak dapat meminta

Gunakan ctx.mode === "tui" sebelum TUI fitur khusus (custom(), pabrik komponen, input terminal). Gunakan ctx.hasUI sebelum dialog dan metode pemberitahuan yang berfungsi dalam mode TUI dan RPC.

Contoh Referensi

Semua contoh di examples/extensions/.

Contoh Keterangan Kunci APIs
Peralatan
hello.ts Registrasi alat minimal registerTool
question.ts Alat dengan interaksi pengguna registerTool, ui.select
questionnaire.ts Alat penyihir multi-langkah registerTool, ui.custom
todo.ts Alat stateful dengan ketekunan registerTool, appendEntry, renderResult, acara sesi
dynamic-tools.ts Daftarkan alat setelah startup dan selama perintah registerTool, session_start, registerCommand
structured-output.ts Alat keluaran terstruktur akhir dengan terminate: true registerTool, menghentikan hasil alat
truncated-tool.ts Contoh pemotongan keluaran registerTool, truncateHead
tool-override.ts Ganti alat baca bawaan registerTool (nama yang sama dengan bawaan)
Perintah
pirate.ts Ubah perintah sistem per putaran registerCommand, before_agent_start
summarize.ts Perintah ringkasan percakapan registerCommand, ui.custom
handoff.ts Penyerahan model lintas penyedia registerCommand, ui.editor, ui.custom
qna.ts Tanya Jawab dengan UI khusus registerCommand, ui.custom, setEditorText
send-user-message.ts Menyuntikkan pesan pengguna registerCommand, sendUserMessage
reload-runtime.ts Muat ulang perintah dan handoff alat LLM registerCommand, ctx.reload(), sendUserMessage
shutdown-command.ts Perintah mematikan dengan baik registerCommand, shutdown()
Acara & Gerbang
permission-gate.ts Blokir perintah berbahaya on("tool_call"), ui.confirm
project-trust.ts Memutuskan atau menunda kepercayaan proyek dari pengguna/ekstensi global atau CLI on("project_trust"), percayai UI, diperlukan hasil kepercayaan
protected-paths.ts Blokir penulisan ke jalur tertentu on("tool_call")
confirm-destructive.ts Konfirmasikan perubahan sesi on("session_before_switch"), on("session_before_fork")
dirty-repo-guard.ts Peringatkan tentang repo git yang kotor on("session_before_*"), exec
input-transform.ts Ubah masukan pengguna on("input")
input-transform-streaming.ts Transformasi masukan yang sadar streaming on("input"), streamingBehavior
model-status.ts React untuk memodelkan perubahan on("model_select"), setStatus
provider-payload.ts Periksa muatan dan header respons penyedia on("before_provider_request"), on("after_provider_response")
system-prompt-header.ts Menampilkan info cepat sistem on("agent_start"), getSystemPrompt
claude-rules.ts Muat aturan dari file on("session_start"), on("before_agent_start")
prompt-customizer.ts Tambahkan panduan alat peka konteks menggunakan systemPromptOptions on("before_agent_start"), BuildSystemPromptOptions
file-trigger.ts Pengamat file memicu pesan sendMessage
Pemadatan & Sesi
custom-compaction.ts Ringkasan pemadatan khusus on("session_before_compact")
trigger-compact.ts Memicu pemadatan secara manual compact()
git-checkpoint.ts Git simpanan di tikungan on("turn_start"), on("session_before_fork"), exec
git-merge-and-resolve.ts Ambil, gabungkan, dan selesaikan konflik on("agent_end"), exec, sendUserMessage
auto-commit-on-exit.ts Berkomitmen untuk mematikan on("session_shutdown"), exec
Komponen UI
status-line.ts Indikator status catatan kaki setStatus, acara sesi
working-indicator.ts Sesuaikan indikator kerja streaming setWorkingIndicator, registerCommand
github-issue-autocomplete.ts Tambahkan #1234 penyelesaian masalah di atas pelengkapan otomatis bawaan dengan memuat terlebih dahulu masalah terbuka terkini dari gh issue list addAutocompleteProvider, on("session_start"), exec
custom-footer.ts Ganti footer seluruhnya registerCommand, setFooter
custom-header.ts Ganti header permulaan on("session_start"), setHeader
modal-editor.ts Editor modal bergaya Vim setEditorComponent, CustomEditor
rainbow-editor.ts Gaya editor khusus setEditorComponent
widget-placement.ts Widget di atas/di bawah editor setWidget
overlay-test.ts Komponen hamparan ui.custom dengan opsi hamparan
overlay-qa-tests.ts Tes overlay yang komprehensif ui.custom, semua opsi hamparan
notify.ts Pemberitahuan sederhana ui.notify
timed-confirm.ts Dialog dengan batas waktu ui.confirm dengan batas waktu/sinyal
mac-system-theme.ts Beralih tema secara otomatis setTheme, exec
Kompleks Extensions
plan-mode/ Implementasi mode rencana penuh Semua jenis acara, registerCommand, registerShortcut, registerFlag, setStatus, setWidget, sendMessage, setActiveTools
preset.ts Preset yang dapat disimpan (model, alat, pemikiran) registerCommand, registerShortcut, registerFlag, setModel, setActiveTools, setThinkingLevel, appendEntry
tools.ts Mengaktifkan/menonaktifkan UI alat registerCommand, setActiveTools, SettingsList, acara sesi
Jarak Jauh & Kotak Pasir
ssh.ts SSH eksekusi jarak jauh registerFlag, on("user_bash"), on("before_agent_start"), pengoperasian alat
interactive-shell.ts Sesi shell yang persisten on("user_bash")
sandbox/ Eksekusi alat dalam kotak pasir Operasi alat
gondolin/ Rutekan alat bawaan dan perintah ! ke dalam Gondolin mikro-VM Pengoperasian alat, penggantian alat bawaan, on("user_bash")
subagent/ Memunculkan sub-agen registerTool, exec
Pertandingan
snake.ts Permainan ular registerCommand, ui.custom, penanganan keyboard
space-invaders.ts Permainan Penjajah Luar Angkasa registerCommand, ui.custom
doom-overlay/ Malapetaka dalam hamparan ui.custom dengan hamparan
Providers
custom-provider-anthropic/ Proksi Antropik Kustom registerProvider
custom-provider-gitlab-duo/ GitIntegrasi Lab Duo registerProvider dengan OAuth
Pesan & Komunikasi
message-renderer.ts Render pesan khusus registerMessageRenderer, sendMessage
entry-renderer.ts TUI-hanya rendering entri khusus registerEntryRenderer, appendEntry
event-bus.ts Acara antar-ekstensi pi.events
Metadata Sesi
session-name.ts Sesi nama untuk pemilih setSessionName, getSessionName
bookmark.ts Tandai entri untuk /pohon setLabel
Lain-lain
inline-bash.ts Sebaris bash dalam panggilan alat on("tool_call")
bash-spawn-hook.ts Sesuaikan perintah bash, cwd, dan env sebelum dieksekusi createBashTool, spawnHook
with-deps/ Ekstensi dengan dependensi npm Struktur paket dengan package.json