chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
@@ -1,6 +1,16 @@
|
||||
# Examples
|
||||
|
||||
Example code for pi-coding-agent SDK and extensions.
|
||||
Example code for the pi-coding-agent SDK, process integration, and extensions.
|
||||
|
||||
## CLI integration
|
||||
|
||||
[`rpc-client.ts`](rpc-client.ts) uses the typed `RpcClient` to run Pi in a child process, stream events, and wait for the run to settle.
|
||||
|
||||
Build the coding-agent package before running it from a repository checkout:
|
||||
|
||||
```bash
|
||||
node examples/rpc-client.ts "Explain this repository"
|
||||
```
|
||||
|
||||
## Directories
|
||||
|
||||
@@ -18,11 +28,9 @@ Example extensions demonstrating:
|
||||
- External integrations (SSH, file watchers, system theme sync)
|
||||
- Custom providers (Anthropic with custom streaming, GitLab Duo)
|
||||
|
||||
### [plugins/pi-example-plugin/](plugins/pi-example-plugin/)
|
||||
An experimental plugin package that Pi automatically builds into separate Session-worker and TUI Chord facets.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [SDK Reference](sdk/README.md)
|
||||
- [SDK Examples](sdk/README.md)
|
||||
- [CLI Integration](../docs/cli-integration.md)
|
||||
- [Extensions Documentation](../docs/extensions.md)
|
||||
- [Skills Documentation](../docs/skills.md)
|
||||
|
||||
@@ -36,7 +36,6 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
|
||||
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
|
||||
| `dynamic-tools.ts` | Register tools after startup (`session_start`) and at runtime via command, with prompt snippets and tool-specific prompt guidelines |
|
||||
| `kimi-deferred-tools.ts` | Search for and progressively activate tools for Kimi's deferred-tool loading protocol |
|
||||
| `structured-output.ts` | Final structured-output tool that returns `terminate: true` so the agent can end on the tool call |
|
||||
| `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior |
|
||||
| `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) |
|
||||
@@ -114,6 +113,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||
|-----------|-------------|
|
||||
| `message-renderer.ts` | Custom message rendering with colors and expandable details via `registerMessageRenderer` |
|
||||
| `entry-renderer.ts` | TUI-only session entry rendering via `appendEntry` and `registerEntryRenderer` |
|
||||
| `debug-provider.ts` | Toggle raw provider stream capture with `/debug-provider` and inspect each assistant message in a TUI-only session entry |
|
||||
| `event-bus.ts` | Inter-extension communication via `pi.events` |
|
||||
|
||||
### Session Metadata
|
||||
@@ -129,6 +129,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
|
||||
|-----------|-------------|
|
||||
| `custom-provider-anthropic/` | Custom Anthropic provider with OAuth support and custom streaming implementation |
|
||||
| `custom-provider-gitlab-duo/` | GitLab Duo provider using pi-ai's built-in Anthropic/OpenAI streaming via proxy |
|
||||
| `jev-router.ts` | Virtual model via `registerVirtualModel` that plans on Codex Sol or Terra (picked by the Jev classifier) and switches to Luna after the first edit |
|
||||
|
||||
### External Dependencies
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
*
|
||||
* How it works:
|
||||
* - registerTool() with the same name as a built-in replaces it entirely
|
||||
* - We create instances of the original tools via createReadTool(), etc.
|
||||
* and delegate execute() to them
|
||||
* - We create the original definitions via createReadToolDefinition(), etc.
|
||||
* and override only their renderers
|
||||
* - renderCall() controls what's shown when the tool is invoked
|
||||
* - renderResult() controls what's shown after execution completes
|
||||
* - renderShell: "self" lets a tool render its own outer shell instead of
|
||||
@@ -26,23 +26,21 @@
|
||||
*/
|
||||
|
||||
import type { BashToolDetails, EditToolDetails, ExtensionAPI, ReadToolDetails } from "@earendil-works/pi-coding-agent";
|
||||
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
createBashToolDefinition,
|
||||
createEditToolDefinition,
|
||||
createReadToolDefinition,
|
||||
createWriteToolDefinition,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { Text } from "@earendil-works/pi-tui";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
// --- Read tool: show path and line count ---
|
||||
const originalRead = createReadTool(cwd);
|
||||
const originalRead = createReadToolDefinition(cwd);
|
||||
pi.registerTool({
|
||||
name: "read",
|
||||
label: "read",
|
||||
description: originalRead.description,
|
||||
parameters: originalRead.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalRead.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...originalRead,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
let text = theme.fg("toolTitle", theme.bold("read "));
|
||||
@@ -92,16 +90,9 @@ export default function (pi: ExtensionAPI) {
|
||||
});
|
||||
|
||||
// --- Bash tool: show command and exit code ---
|
||||
const originalBash = createBashTool(cwd);
|
||||
const originalBash = createBashToolDefinition(cwd);
|
||||
pi.registerTool({
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: originalBash.description,
|
||||
parameters: originalBash.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalBash.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...originalBash,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
let text = theme.fg("toolTitle", theme.bold("$ "));
|
||||
@@ -151,17 +142,9 @@ export default function (pi: ExtensionAPI) {
|
||||
});
|
||||
|
||||
// --- Edit tool: show path and diff stats ---
|
||||
const originalEdit = createEditTool(cwd);
|
||||
const originalEdit = createEditToolDefinition(cwd);
|
||||
pi.registerTool({
|
||||
name: "edit",
|
||||
label: "edit",
|
||||
description: originalEdit.description,
|
||||
parameters: originalEdit.parameters,
|
||||
renderShell: "self",
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalEdit.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...originalEdit,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
let text = theme.fg("toolTitle", theme.bold("edit "));
|
||||
@@ -216,16 +199,9 @@ export default function (pi: ExtensionAPI) {
|
||||
});
|
||||
|
||||
// --- Write tool: show path and size ---
|
||||
const originalWrite = createWriteTool(cwd);
|
||||
const originalWrite = createWriteToolDefinition(cwd);
|
||||
pi.registerTool({
|
||||
name: "write",
|
||||
label: "write",
|
||||
description: originalWrite.description,
|
||||
parameters: originalWrite.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalWrite.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...originalWrite,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
let text = theme.fg("toolTitle", theme.bold("write "));
|
||||
|
||||
@@ -27,9 +27,11 @@ import {
|
||||
type Api,
|
||||
type AssistantMessage,
|
||||
type AssistantMessageEventStream,
|
||||
type Context,
|
||||
calculateCost,
|
||||
collapseSystemMessages,
|
||||
createAssistantMessageEventStream,
|
||||
getCurrentSystemPrompt,
|
||||
getCurrentTools,
|
||||
type ImageContent,
|
||||
type Message,
|
||||
type Model,
|
||||
@@ -42,6 +44,7 @@ import {
|
||||
type Tool,
|
||||
type ToolCall,
|
||||
type ToolResultMessage,
|
||||
type TranscriptContext,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
@@ -334,10 +337,15 @@ function mapStopReason(reason: string): StopReason {
|
||||
|
||||
function streamCustomAnthropic(
|
||||
model: Model<Api>,
|
||||
context: Context,
|
||||
context: TranscriptContext,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream {
|
||||
const stream = createAssistantMessageEventStream();
|
||||
// The transcript carries the prompt and tools in its system messages. This provider sends
|
||||
// one top-level system prompt, so fold later system messages into the leading one first.
|
||||
const transcript = collapseSystemMessages(context);
|
||||
const systemPrompt = getCurrentSystemPrompt(transcript.messages);
|
||||
const tools = getCurrentTools(transcript.messages);
|
||||
|
||||
(async () => {
|
||||
const output: AssistantMessage = {
|
||||
@@ -393,7 +401,7 @@ function streamCustomAnthropic(
|
||||
// Build request params
|
||||
const params: MessageCreateParamsStreaming = {
|
||||
model: model.id,
|
||||
messages: convertMessages(context.messages, isOAuth, context.tools),
|
||||
messages: convertMessages(transcript.messages, isOAuth, tools),
|
||||
max_tokens: options?.maxTokens || Math.floor(model.maxTokens / 3),
|
||||
stream: true,
|
||||
};
|
||||
@@ -407,25 +415,25 @@ function streamCustomAnthropic(
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
];
|
||||
if (context.systemPrompt) {
|
||||
if (systemPrompt) {
|
||||
params.system.push({
|
||||
type: "text",
|
||||
text: sanitizeSurrogates(context.systemPrompt),
|
||||
text: sanitizeSurrogates(systemPrompt),
|
||||
cache_control: { type: "ephemeral" },
|
||||
});
|
||||
}
|
||||
} else if (context.systemPrompt) {
|
||||
} else if (systemPrompt) {
|
||||
params.system = [
|
||||
{
|
||||
type: "text",
|
||||
text: sanitizeSurrogates(context.systemPrompt),
|
||||
text: sanitizeSurrogates(systemPrompt),
|
||||
cache_control: { type: "ephemeral" },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (context.tools) {
|
||||
params.tools = convertTools(context.tools, isOAuth);
|
||||
if (tools.length > 0) {
|
||||
params.tools = convertTools(tools, isOAuth);
|
||||
}
|
||||
|
||||
// Handle thinking/reasoning
|
||||
@@ -474,9 +482,7 @@ function streamCustomAnthropic(
|
||||
output.content.push({
|
||||
type: "toolCall",
|
||||
id: event.content_block.id,
|
||||
name: isOAuth
|
||||
? fromClaudeCodeName(event.content_block.name, context.tools)
|
||||
: event.content_block.name,
|
||||
name: isOAuth ? fromClaudeCodeName(event.content_block.name, tools) : event.content_block.name,
|
||||
arguments: {},
|
||||
partialJson: "",
|
||||
index: event.index,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"private": true,
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
type Api,
|
||||
type AssistantMessageEventStream,
|
||||
anthropicMessagesApi,
|
||||
type Context,
|
||||
createAssistantMessageEventStream,
|
||||
type Model,
|
||||
type OAuthCredentials,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
openAIResponsesApi,
|
||||
type SimpleStreamOptions,
|
||||
type ThinkingLevelMap,
|
||||
type TranscriptContext,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
@@ -306,7 +306,7 @@ async function refreshGitLabToken(credentials: OAuthCredentials, signal: AbortSi
|
||||
|
||||
export function streamGitLabDuo(
|
||||
model: Model<Api>,
|
||||
context: Context,
|
||||
context: TranscriptContext,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream {
|
||||
const stream = createAssistantMessageEventStream();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"private": true,
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* Test script for GitLab Duo extension
|
||||
* Run: npx tsx test.ts [model-id] [--thinking]
|
||||
* Run: node test.ts [model-id] [--thinking]
|
||||
*
|
||||
* Examples:
|
||||
* npx tsx test.ts # Test default (claude-sonnet-4-5-20250929)
|
||||
* npx tsx test.ts gpt-5-codex # Test GPT-5 Codex
|
||||
* npx tsx test.ts claude-sonnet-4-5-20250929 --thinking
|
||||
* node test.ts # Test default (claude-sonnet-4-5-20250929)
|
||||
* node test.ts gpt-5-codex # Test GPT-5 Codex
|
||||
* node test.ts claude-sonnet-4-5-20250929 --thinking
|
||||
*/
|
||||
|
||||
import { type Api, type Context, type Model, registerApiProvider, streamSimple } from "@earendil-works/pi-ai/compat";
|
||||
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
||||
import { readFileSync } from "fs";
|
||||
import { getAgentDir } from "packages/coding-agent/src/config.js";
|
||||
import { join } from "path";
|
||||
import { MODELS, streamGitLabDuo } from "./index.ts";
|
||||
|
||||
|
||||
90
third_party/pi/packages/coding-agent/examples/extensions/debug-provider.ts
vendored
Normal file
90
third_party/pi/packages/coding-agent/examples/extensions/debug-provider.ts
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Raw provider event viewer.
|
||||
*
|
||||
* Usage: /debug-provider [on|off]
|
||||
* With no argument, the command toggles capture. Captured events are persisted
|
||||
* as expandable custom entries.
|
||||
*/
|
||||
|
||||
import { type ExtensionAPI, keyHint } from "@earendil-works/pi-coding-agent";
|
||||
import { Box, Text } from "@earendil-works/pi-tui";
|
||||
|
||||
const ENTRY_TYPE = "debug-provider-events";
|
||||
const STATUS_KEY = "debug-provider";
|
||||
|
||||
interface ProviderDebugEntry {
|
||||
provider: string;
|
||||
api: string;
|
||||
model: string;
|
||||
events: unknown[];
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
let enabled = false;
|
||||
let activeEvents: unknown[] | undefined;
|
||||
let completedEntry: ProviderDebugEntry | undefined;
|
||||
|
||||
pi.registerEntryRenderer<ProviderDebugEntry>(ENTRY_TYPE, (entry, { expanded }, theme) => {
|
||||
const data = entry.data;
|
||||
if (!data) return new Text(theme.fg("warning", "[provider debug] Missing event data"), 0, 0);
|
||||
|
||||
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
||||
const count = `${data.events.length} event${data.events.length === 1 ? "" : "s"}`;
|
||||
const expandHint = expanded ? "" : ` (${keyHint("app.tools.expand", "to view events")})`;
|
||||
box.addChild(
|
||||
new Text(
|
||||
`${theme.fg("accent", "[provider debug]")} ${data.provider}/${data.model} (${data.api}) · ${count}${expandHint}`,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
if (expanded) {
|
||||
box.addChild(new Text(JSON.stringify(data.events, null, 2), 0, 0));
|
||||
}
|
||||
return box;
|
||||
});
|
||||
|
||||
pi.registerCommand("debug-provider", {
|
||||
description: "Toggle capture of raw provider stream events",
|
||||
handler: async (args, ctx) => {
|
||||
const requestedState = args.trim().toLowerCase();
|
||||
if (requestedState !== "" && requestedState !== "on" && requestedState !== "off") {
|
||||
ctx.ui.notify("Usage: /debug-provider [on|off]", "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
enabled = requestedState === "" ? !enabled : requestedState === "on";
|
||||
if (!enabled) {
|
||||
activeEvents = undefined;
|
||||
completedEntry = undefined;
|
||||
}
|
||||
ctx.ui.setStatus(STATUS_KEY, enabled ? "provider debug" : undefined);
|
||||
ctx.ui.notify(`Provider event capture ${enabled ? "enabled" : "disabled"}`, "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.on("turn_start", () => {
|
||||
activeEvents = enabled ? [] : undefined;
|
||||
});
|
||||
|
||||
pi.on("provider_stream_event", (event) => {
|
||||
activeEvents?.push(structuredClone(event.data));
|
||||
});
|
||||
|
||||
pi.on("message_end", (event) => {
|
||||
if (event.message.role !== "assistant" || !activeEvents) return;
|
||||
completedEntry = {
|
||||
provider: event.message.provider,
|
||||
api: event.message.api,
|
||||
model: event.message.model,
|
||||
events: activeEvents,
|
||||
};
|
||||
activeEvents = undefined;
|
||||
});
|
||||
|
||||
pi.on("turn_end", () => {
|
||||
if (!completedEntry) return;
|
||||
pi.appendEntry<ProviderDebugEntry>(ENTRY_TYPE, completedEntry);
|
||||
completedEntry = undefined;
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
||||
"$schema": "https://pi.dev/schemas/theme.schema.json",
|
||||
"name": "dynamic-resources",
|
||||
"vars": {
|
||||
"cyan": "#00d7ff",
|
||||
|
||||
4
third_party/pi/packages/coding-agent/examples/extensions/gondolin/package-lock.json
generated
vendored
4
third_party/pi/packages/coding-agent/examples/extensions/gondolin/package-lock.json
generated
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-gondolin",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-gondolin",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@earendil-works/gondolin": "0.12.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-gondolin",
|
||||
"private": true,
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
113
third_party/pi/packages/coding-agent/examples/extensions/jev-router.ts
vendored
Normal file
113
third_party/pi/packages/coding-agent/examples/extensions/jev-router.ts
vendored
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Jev router - a virtual model that plans on a strong model and implements on a cheap one.
|
||||
*
|
||||
* Registers `jev/auto`, which routes between three OpenAI Codex models:
|
||||
*
|
||||
* - Planning: GPT-5.6 Sol for complex work, GPT-5.6 Terra otherwise. The Jev classifier rates the
|
||||
* first user message; planning stays on the chosen model.
|
||||
* - Implementation: GPT-5.6 Luna.
|
||||
*
|
||||
* The planning model explores, plans, and makes the first edit. After the first successful `edit`
|
||||
* or `write` tool call, the next request of the same turn goes to Luna, and the session stays
|
||||
* there. A session therefore switches models once and accepts a single prompt-cache miss.
|
||||
*
|
||||
* The phase is router state: Pi stores it on the session branch, so it follows the session tree
|
||||
* and survives compaction. The selected thinking level passes through as the reasoning effort of
|
||||
* the chosen model. Requests outside the agent loop, such as compaction summaries, go to Luna.
|
||||
*
|
||||
* Requires TypeSafe credentials (TYPESAFE_API_KEY) and an OpenAI Codex login.
|
||||
* Usage: pi -e ./jev-router.ts --model jev/auto
|
||||
*/
|
||||
|
||||
import type { Message } from "@earendil-works/pi-ai";
|
||||
import type { ExtensionAPI, ExtensionContext, ModelRoute, ModelRouteRequest } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const PROVIDER = "openai-codex";
|
||||
const SOL = "gpt-5.6-sol";
|
||||
const TERRA = "gpt-5.6-terra";
|
||||
const LUNA = "gpt-5.6-luna";
|
||||
|
||||
/** Tools whose successful result means implementation has started. */
|
||||
const EDIT_TOOLS = new Set(["edit", "write"]);
|
||||
|
||||
interface JevState {
|
||||
phase: "planning" | "implementation";
|
||||
/** OpenAI Codex model for this phase. */
|
||||
model: string;
|
||||
}
|
||||
|
||||
type JevRequest = ModelRouteRequest<JevState>;
|
||||
|
||||
function routeTo(request: JevRequest, ctx: ExtensionContext, id: string, state?: JevState): ModelRoute<JevState> {
|
||||
const model = ctx.modelRegistry.find(PROVIDER, id);
|
||||
if (!model) throw new Error(`Model ${PROVIDER}/${id} is not in the catalog`);
|
||||
return { model, thinkingLevel: request.thinkingLevel, state };
|
||||
}
|
||||
|
||||
function lastUserText(messages: readonly Message[]): string {
|
||||
const content = messages.filter((message) => message.role === "user").at(-1)?.content ?? "";
|
||||
if (typeof content === "string") return content;
|
||||
return content.flatMap((block) => (block.type === "text" ? [block.text] : [])).join("\n");
|
||||
}
|
||||
|
||||
/** Whether a tool call since the last user message edited a file successfully. */
|
||||
function editedThisTurn(messages: readonly Message[]): boolean {
|
||||
const lastUser = messages.findLastIndex((message) => message.role === "user");
|
||||
return messages
|
||||
.slice(lastUser + 1)
|
||||
.some((message) => message.role === "toolResult" && EDIT_TOOLS.has(message.toolName) && !message.isError);
|
||||
}
|
||||
|
||||
/** Planning model for a new session: Sol for complex work, Terra otherwise or when Jev is unavailable. */
|
||||
async function choosePlanningModel(request: JevRequest, ctx: ExtensionContext): Promise<string> {
|
||||
// Keep a planning model the session already uses, so switching to jev/auto costs no cache miss.
|
||||
const previous = request.previous?.model;
|
||||
if (previous?.provider === PROVIDER && (previous.id === SOL || previous.id === TERRA)) return previous.id;
|
||||
|
||||
const jev = ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest");
|
||||
if (!jev) return TERRA;
|
||||
const result = await ctx.modelRegistry.classify(
|
||||
jev,
|
||||
{
|
||||
state: { prompt: lastUserText(request.messages).slice(0, 16_000) },
|
||||
questions: {
|
||||
complexity: {
|
||||
type: "choice",
|
||||
instructions: "How demanding is the software engineering work requested in `prompt`?",
|
||||
criteria: {
|
||||
standard: "Ordinary features, fixes, reviews, or questions",
|
||||
complex: "Subtle design, cross-cutting changes, or hard debugging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ signal: request.signal },
|
||||
);
|
||||
const answer = result.stopReason === "stop" ? result.answers.complexity : undefined;
|
||||
return answer?.type === "choice" && (answer.probabilities.complex ?? 0) >= 0.5 ? SOL : TERRA;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerVirtualModel<JevState>({
|
||||
provider: "jev",
|
||||
id: "auto",
|
||||
name: "Auto (Jev)",
|
||||
thinkingLevels: ["low", "medium", "high", "xhigh"],
|
||||
// Shared by all three models; shown before the first response.
|
||||
contextWindow: 272_000,
|
||||
maxTokens: 128_000,
|
||||
async route(request, ctx) {
|
||||
if (request.reason === "direct") return routeTo(request, ctx, LUNA);
|
||||
const state = request.state;
|
||||
if (!state) {
|
||||
const model = await choosePlanningModel(request, ctx);
|
||||
return routeTo(request, ctx, model, { phase: "planning", model });
|
||||
}
|
||||
// The planning model made the first edit: hand the rest of the work to Luna.
|
||||
if (state.phase === "planning" && editedThisTurn(request.messages)) {
|
||||
return routeTo(request, ctx, LUNA, { phase: "implementation", model: LUNA });
|
||||
}
|
||||
return routeTo(request, ctx, state.model);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* Minimal Kimi deferred-tool loading demo.
|
||||
*
|
||||
* pi -e ./kimi-deferred-tools.ts
|
||||
* example prompt: Use the available tools to calculate 100 + 500. Do not calculate it yourself.
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
|
||||
function calculate(_expr: string): string {
|
||||
return "42";
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI): void {
|
||||
pi.registerTool({
|
||||
name: "Calculator",
|
||||
label: "Calculator",
|
||||
description: "Evaluate a simple arithmetic expression.",
|
||||
parameters: Type.Object({
|
||||
expr: Type.String({ description: "An expression such as 100 + 500" }),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
return {
|
||||
content: [{ type: "text", text: calculate(params.expr) }],
|
||||
details: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "tool_search",
|
||||
label: "Tool Search",
|
||||
description: "Find and activate tools for a capability.",
|
||||
promptSnippet: "Search for additional tools when the active tools cannot perform the task",
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "Capability to search for" }),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
if (!params.query.toLowerCase().includes("calc")) {
|
||||
return {
|
||||
content: [{ type: "text", text: "The relevant tools do not exist." }],
|
||||
details: { matches: [], added: [] },
|
||||
};
|
||||
}
|
||||
|
||||
const active = pi.getActiveTools();
|
||||
const added = active.includes("Calculator") ? [] : ["Calculator"];
|
||||
if (added.length > 0) pi.setActiveTools([...active, ...added]);
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text: "Success. Found 1 matching tool(s)" }],
|
||||
details: { matches: ["Calculator"], added },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.on("session_start", () => {
|
||||
pi.setActiveTools(["tool_search"]);
|
||||
});
|
||||
}
|
||||
@@ -18,13 +18,14 @@
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import {
|
||||
createBashTool,
|
||||
createEditTool,
|
||||
createFindTool,
|
||||
createGrepTool,
|
||||
createLsTool,
|
||||
createReadTool,
|
||||
createWriteTool,
|
||||
createBashToolDefinition,
|
||||
createEditToolDefinition,
|
||||
createFindToolDefinition,
|
||||
createGrepToolDefinition,
|
||||
createLsToolDefinition,
|
||||
createReadToolDefinition,
|
||||
createWriteToolDefinition,
|
||||
formatReadLineRange,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
import { Text } from "@earendil-works/pi-tui";
|
||||
import { homedir } from "os";
|
||||
@@ -40,55 +41,34 @@ function shortenPath(path: string): string {
|
||||
return path;
|
||||
}
|
||||
|
||||
// Cache for built-in tools by cwd
|
||||
const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
|
||||
|
||||
function createBuiltInTools(cwd: string) {
|
||||
return {
|
||||
read: createReadTool(cwd),
|
||||
bash: createBashTool(cwd),
|
||||
edit: createEditTool(cwd),
|
||||
write: createWriteTool(cwd),
|
||||
find: createFindTool(cwd),
|
||||
grep: createGrepTool(cwd),
|
||||
ls: createLsTool(cwd),
|
||||
read: createReadToolDefinition(cwd),
|
||||
bash: createBashToolDefinition(cwd),
|
||||
edit: createEditToolDefinition(cwd),
|
||||
write: createWriteToolDefinition(cwd),
|
||||
find: createFindToolDefinition(cwd),
|
||||
grep: createGrepToolDefinition(cwd),
|
||||
ls: createLsToolDefinition(cwd),
|
||||
};
|
||||
}
|
||||
|
||||
function getBuiltInTools(cwd: string) {
|
||||
let tools = toolCache.get(cwd);
|
||||
if (!tools) {
|
||||
tools = createBuiltInTools(cwd);
|
||||
toolCache.set(cwd, tools);
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const tools = createBuiltInTools(process.cwd());
|
||||
|
||||
// =========================================================================
|
||||
// Read Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "read",
|
||||
label: "read",
|
||||
description:
|
||||
"Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to 2000 lines or 50KB (whichever is hit first). Use offset/limit for large files.",
|
||||
parameters: getBuiltInTools(process.cwd()).read.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.read.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.read,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const path = shortenPath(args.path || "");
|
||||
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
|
||||
|
||||
// Show line range if specified
|
||||
if (args.offset !== undefined || args.limit !== undefined) {
|
||||
const startLine = args.offset ?? 1;
|
||||
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
||||
pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
||||
const lineRange = formatReadLineRange(args.offset, args.limit);
|
||||
if (lineRange) {
|
||||
pathDisplay += theme.fg("warning", lineRange);
|
||||
}
|
||||
|
||||
return new Text(`${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`, 0, 0);
|
||||
@@ -116,16 +96,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Bash Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description:
|
||||
"Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last 2000 lines or 50KB (whichever is hit first).",
|
||||
parameters: getBuiltInTools(process.cwd()).bash.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.bash.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.bash,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const command = args.command || "...";
|
||||
@@ -165,16 +136,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Write Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "write",
|
||||
label: "write",
|
||||
description:
|
||||
"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
||||
parameters: getBuiltInTools(process.cwd()).write.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.write.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.write,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const path = shortenPath(args.path || "");
|
||||
@@ -207,16 +169,8 @@ export default function (pi: ExtensionAPI) {
|
||||
// Edit Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "edit",
|
||||
label: "edit",
|
||||
description:
|
||||
"Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits.",
|
||||
parameters: getBuiltInTools(process.cwd()).edit.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.edit.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.edit,
|
||||
renderShell: "default",
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const path = shortenPath(args.path || "");
|
||||
@@ -252,16 +206,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Find Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "find",
|
||||
label: "find",
|
||||
description:
|
||||
"Find files by name pattern (glob). Searches recursively from the specified path. Output limited to 200 results.",
|
||||
parameters: getBuiltInTools(process.cwd()).find.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.find.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.find,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const pattern = args.pattern || "";
|
||||
@@ -310,16 +255,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Grep Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "grep",
|
||||
label: "grep",
|
||||
description:
|
||||
"Search file contents by regex pattern. Uses ripgrep for fast searching. Output limited to 200 matches.",
|
||||
parameters: getBuiltInTools(process.cwd()).grep.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.grep.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.grep,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const pattern = args.pattern || "";
|
||||
@@ -372,16 +308,7 @@ export default function (pi: ExtensionAPI) {
|
||||
// Ls Tool
|
||||
// =========================================================================
|
||||
pi.registerTool({
|
||||
name: "ls",
|
||||
label: "ls",
|
||||
description:
|
||||
"List directory contents with file sizes. Shows files and directories with their sizes. Output limited to 500 entries.",
|
||||
parameters: getBuiltInTools(process.cwd()).ls.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||
const tools = getBuiltInTools(ctx.cwd);
|
||||
return tools.ls.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
...tools.ls,
|
||||
|
||||
renderCall(args, theme, _context) {
|
||||
const path = shortenPath(args.path || ".");
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
||||
import { CURSOR_MARKER, type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
||||
import { CURSOR_MARKER, type Focusable, matchesKey, renderFakeCursor, visibleWidth } from "@earendil-works/pi-tui";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerCommand("overlay-test", {
|
||||
@@ -131,7 +131,7 @@ class OverlayTestComponent implements Focusable {
|
||||
const after = inputDisplay.slice(item.cursor + 1);
|
||||
// Emit hardware cursor marker for IME support when focused
|
||||
const marker = this.focused ? CURSOR_MARKER : "";
|
||||
inputDisplay = `${before}${marker}\x1b[7m${cursorChar}\x1b[27m${after}`;
|
||||
inputDisplay = `${before}${marker}${renderFakeCursor(cursorChar)}${after}`;
|
||||
}
|
||||
content = `${prefix + label} ${inputDisplay}`;
|
||||
} else {
|
||||
|
||||
@@ -197,8 +197,12 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
||||
};
|
||||
});
|
||||
|
||||
// Inject plan/execution context before agent starts
|
||||
pi.on("before_agent_start", async () => {
|
||||
// Inject plan/execution context before a user prompt. Runs started by custom messages, such as
|
||||
// background task notifications, skip it: the context from the last prompt is still in the
|
||||
// conversation, and adding it again on every notification would only repeat it.
|
||||
pi.on("before_agent_start", async (event) => {
|
||||
if (event.messageType !== "user") return;
|
||||
|
||||
if (planModeEnabled) {
|
||||
return {
|
||||
message: {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/**
|
||||
* Prompt Customizer Extension
|
||||
*
|
||||
* Demonstrates using systemPromptOptions to make informed, context-aware
|
||||
* modifications to the system prompt without re-discovering resources.
|
||||
*
|
||||
* This extension adds tool-specific guidance based on what tools and skills
|
||||
* are currently active, respecting whatever the user has configured.
|
||||
* Demonstrates using systemPromptOptions to add context-aware prompt sections
|
||||
* without replacing or reparsing the complete rendered prompt.
|
||||
*
|
||||
* Usage:
|
||||
* 1. Copy this file to ~/.pi/agent/extensions/ or your project's .pi/extensions/
|
||||
@@ -14,84 +11,39 @@
|
||||
|
||||
import type { BuildSystemPromptOptions, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
/**
|
||||
* Adds tool-specific guidance that adapts to the active tool set.
|
||||
* Instead of appending one-size-fits-all instructions, this reads what's
|
||||
* actually loaded and tailors the guidance accordingly.
|
||||
*/
|
||||
function addToolGuidance(options: BuildSystemPromptOptions, basePrompt: string): string {
|
||||
function buildToolGuidance(options: BuildSystemPromptOptions): string {
|
||||
const hasTool = (name: string) => options.selectedTools?.includes(name) ?? false;
|
||||
|
||||
const parts: string[] = [];
|
||||
const rules: string[] = [];
|
||||
|
||||
if (hasTool("read")) {
|
||||
parts.push(
|
||||
"• Use the `read` tool for file contents (supports text and images).",
|
||||
" - For large files, use `offset` and `limit` to read in chunks.",
|
||||
rules.push(
|
||||
"- Use `read` for file contents; it supports text and images.",
|
||||
"- For large files, use `offset` and `limit` to read in chunks.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasTool("bash")) {
|
||||
parts.push("• Execute commands with the `bash` tool. Use it for file operations like `ls`, `find`, `grep`.");
|
||||
rules.push("- Use `bash` for file operations such as `ls`, `find`, and `grep`.");
|
||||
}
|
||||
|
||||
if (hasTool("edit")) {
|
||||
parts.push(
|
||||
"• Use the `edit` tool for precise text replacements in files. Match exact content including whitespace.",
|
||||
);
|
||||
rules.push("- Use `edit` for precise text replacements that match existing content exactly.");
|
||||
}
|
||||
|
||||
if (hasTool("write")) {
|
||||
parts.push("• Use the `write` tool to create new files or overwrite existing ones completely.");
|
||||
rules.push("- Use `write` to create new files or replace existing files completely.");
|
||||
}
|
||||
|
||||
if (options.skills && options.skills.length > 0) {
|
||||
const skillNames = options.skills.map((s) => s.name).join(", ");
|
||||
parts.push(`\nAvailable skills: ${skillNames}`, "Use skill documentation for best practices on specific tools.");
|
||||
rules.push(`- Available skills: ${options.skills.map((skill) => skill.name).join(", ")}.`);
|
||||
}
|
||||
|
||||
if (parts.length === 0) {
|
||||
return basePrompt;
|
||||
}
|
||||
|
||||
return `${basePrompt}
|
||||
|
||||
## Tool Guidance
|
||||
|
||||
${parts.join("\n")}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges extension instructions with user-provided append prompts.
|
||||
* This respects whatever the user configured via --append-system-prompt
|
||||
* flags or files, rather than duplicating that work.
|
||||
*/
|
||||
function mergeWithUserAppend(options: BuildSystemPromptOptions): string {
|
||||
const userAppend = options.appendSystemPrompt;
|
||||
const extensionSpecific = `
|
||||
## Extension-Added Context
|
||||
|
||||
This prompt includes tool guidance and skill information loaded dynamically.
|
||||
If you have additional requirements, configure them via --append-system-prompt or project context files.
|
||||
`;
|
||||
|
||||
if (userAppend) {
|
||||
return `${userAppend}\n\n${extensionSpecific}`;
|
||||
}
|
||||
|
||||
return extensionSpecific;
|
||||
return rules.join("\n");
|
||||
}
|
||||
|
||||
export default function promptCustomizer(pi: ExtensionAPI) {
|
||||
pi.on("before_agent_start", async (event) => {
|
||||
const { systemPrompt, systemPromptOptions } = event;
|
||||
|
||||
const customPrompt = addToolGuidance(systemPromptOptions, systemPrompt);
|
||||
const appendSection = mergeWithUserAppend(systemPromptOptions);
|
||||
|
||||
return {
|
||||
systemPrompt: `${customPrompt}${appendSection}`,
|
||||
};
|
||||
pi.on("before_agent_start", (event) => {
|
||||
const guidance = buildToolGuidance(event.systemPromptOptions);
|
||||
if (guidance) {
|
||||
event.systemPromptOptions.sections.tool_guidance = guidance;
|
||||
} else {
|
||||
delete event.systemPromptOptions.sections.tool_guidance;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
4
third_party/pi/packages/coding-agent/examples/extensions/sandbox/package-lock.json
generated
vendored
4
third_party/pi/packages/coding-agent/examples/extensions/sandbox/package-lock.json
generated
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.15.1",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.15.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"private": true,
|
||||
"version": "1.15.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -22,6 +22,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
||||
import {
|
||||
CONFIG_DIR_NAME,
|
||||
type ExtensionAPI,
|
||||
formatReadLineRange,
|
||||
getAgentDir,
|
||||
getMarkdownTheme,
|
||||
withFileMutationQueue,
|
||||
@@ -87,13 +88,10 @@ function formatToolCall(
|
||||
case "read": {
|
||||
const rawPath = (args.file_path || args.path || "...") as string;
|
||||
const filePath = shortenPath(rawPath);
|
||||
const offset = args.offset as number | undefined;
|
||||
const limit = args.limit as number | undefined;
|
||||
const lineRange = formatReadLineRange(args.offset, args.limit);
|
||||
let text = themeFg("accent", filePath);
|
||||
if (offset !== undefined || limit !== undefined) {
|
||||
const startLine = offset ?? 1;
|
||||
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
||||
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
||||
if (lineRange) {
|
||||
text += themeFg("warning", lineRange);
|
||||
}
|
||||
return themeFg("muted", "read ") + text;
|
||||
}
|
||||
|
||||
4
third_party/pi/packages/coding-agent/examples/extensions/with-deps/package-lock.json
generated
vendored
4
third_party/pi/packages/coding-agent/examples/extensions/with-deps/package-lock.json
generated
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"private": true,
|
||||
"version": "0.85.1",
|
||||
"version": "1.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
# `@earendil-works/pi-example-plugin`
|
||||
|
||||
This package provides conventional `session` and `tui` Chord facets. The Session-worker facet provides a remote greeting service. The TUI facet contributes `/hello` and calls that service.
|
||||
|
||||
The package needs no build script. Pi asks Chord to discover `src/session.ts` and `src/tui.ts`, builds both entries into its server-owned plugin cache, and sends the TUI artifact to clients.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```bash
|
||||
PI_EXPERIMENTAL=1 ./pi-test.sh server \
|
||||
-e "$PWD/packages/coding-agent/examples/plugins/pi-example-plugin"
|
||||
```
|
||||
|
||||
Alternatively, a client can select the plugin for the Session it creates or resumes on one local server:
|
||||
|
||||
```bash
|
||||
PI_EXPERIMENTAL=1 ./pi-test.sh client \
|
||||
-e "$PWD/packages/coding-agent/examples/plugins/pi-example-plugin"
|
||||
```
|
||||
|
||||
Repeat `-e` to select multiple plugin packages. Client paths are resolved locally and sent only to a Unix server; Radius clients cannot select server filesystem paths. The Session and matching TUI facets are stored with that Session, so later server generations and clients can resume it without plugin arguments. Other Sessions and their workers are unaffected. An active Session rejects a different package selection instead of being restarted.
|
||||
|
||||
`server -e` establishes the server profile's default Session and TUI facets. Starting an explicit foreground server without `-e` clears that default. Client selection never changes the server's root facet generation.
|
||||
|
||||
Run `/hello Armin` in the TUI. After editing a facet, run `/reload`. The server atomically rebuilds the package, reloads the attached Session-worker generation, updates the current TUI generation, and serves the new artifact to future clients.
|
||||
|
||||
Package metadata can override or disable conventional entries:
|
||||
|
||||
```json
|
||||
{
|
||||
"chord": {
|
||||
"facets": {
|
||||
"session": "./src/worker.ts",
|
||||
"tui": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "@earendil-works/pi-example-plugin",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"peerDependencies": {
|
||||
"@earendil-works/chord": "^0.84.4",
|
||||
"@earendil-works/pi-coding-agent": "^0.84.4"
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { type Context, defineService, type ReplicatedState } from "@earendil-works/chord";
|
||||
|
||||
export interface ExampleFacetReply {
|
||||
readonly message: string;
|
||||
readonly workerActivations: number;
|
||||
}
|
||||
|
||||
export interface ExampleFacetService {
|
||||
readonly workerActivations: ReplicatedState<{ count: number }>;
|
||||
greet(input: { readonly name: string }, context: Context): Promise<ExampleFacetReply>;
|
||||
}
|
||||
|
||||
export const ExampleFacetService = defineService<ExampleFacetService>("pi.example-plugin.greeting");
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineFacet } from "@earendil-works/chord";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { ExampleFacetService } from "./contract.ts";
|
||||
|
||||
export default defineFacet({
|
||||
id: "@earendil-works/pi-example-plugin/session",
|
||||
setup(env) {
|
||||
const workerActivations = env.replicatedState({ count: 0 });
|
||||
env.provide(ExampleFacetService, {
|
||||
workerActivations,
|
||||
async greet({ name }) {
|
||||
return {
|
||||
message: `Hello ${name} from the bundled Session worker facet!!!`,
|
||||
workerActivations: workerActivations.value.count,
|
||||
};
|
||||
},
|
||||
});
|
||||
env.onActivate(() => {
|
||||
workerActivations.state.count += 1;
|
||||
workerActivations.publish(BACKGROUND_CONTEXT);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineFacet } from "@earendil-works/chord";
|
||||
import { AgentController, PresentationUI, SlashCommands } from "@earendil-works/pi-coding-agent/experimental/plugin";
|
||||
import { ExampleFacetService } from "./contract.ts";
|
||||
|
||||
export default defineFacet({
|
||||
id: "@earendil-works/pi-example-plugin/tui",
|
||||
setup(env) {
|
||||
const example = env.use(ExampleFacetService);
|
||||
const commands = env.use(SlashCommands);
|
||||
const controller = env.use(AgentController);
|
||||
const ui = env.use(PresentationUI);
|
||||
env.onActivate(() => {
|
||||
env.own(
|
||||
commands.replace({
|
||||
name: "hello",
|
||||
description: "Call the bundled Session worker facet",
|
||||
argumentHint: "<name>",
|
||||
async run(args, context) {
|
||||
const message = args.length === 0 ? "from the TUI" : args;
|
||||
const reply = await example.greet({ name: message }, context);
|
||||
ui.showStatus(
|
||||
`We got this: ${reply.message} Worker activations: ${reply.workerActivations}.`,
|
||||
context,
|
||||
);
|
||||
return controller.prompt({ message: `pong: ${message}`, images: null }, context);
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
35
third_party/pi/packages/coding-agent/examples/rpc-client.ts
vendored
Normal file
35
third_party/pi/packages/coding-agent/examples/rpc-client.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Run one prompt through a Pi RPC child process.
|
||||
*
|
||||
* Build the coding-agent package first, then run:
|
||||
* node examples/rpc-client.ts "Explain this repository"
|
||||
*/
|
||||
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RpcClient } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const exampleDirectory = dirname(fileURLToPath(import.meta.url));
|
||||
const prompt = process.argv.slice(2).join(" ") || "Explain this repository in one paragraph.";
|
||||
|
||||
const client = new RpcClient({
|
||||
cliPath: join(exampleDirectory, "../dist/cli.js"),
|
||||
args: ["--no-session"],
|
||||
});
|
||||
|
||||
const unsubscribe = client.onEvent((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
} else if (event.type === "tool_execution_start") {
|
||||
process.stderr.write(`\n[tool: ${event.toolName}]\n`);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await client.start();
|
||||
await client.promptAndWait(prompt);
|
||||
process.stdout.write("\n");
|
||||
} finally {
|
||||
unsubscribe();
|
||||
await client.stop();
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
* Demonstrates how to build a custom UI on top of the RPC protocol,
|
||||
* including handling extension UI requests (select, confirm, input, editor).
|
||||
*
|
||||
* Usage: npx tsx examples/rpc-extension-ui.ts
|
||||
* Usage: node examples/rpc-extension-ui.ts
|
||||
*
|
||||
* Slash commands:
|
||||
* /select - demo select dialog
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import * as readline from "node:readline";
|
||||
import { StringDecoder } from "node:string_decoder";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
type Component,
|
||||
@@ -258,7 +258,7 @@ async function main() {
|
||||
|
||||
const agent = spawn(
|
||||
"node",
|
||||
[cliPath, "--mode", "rpc", "--no-session", "--no-extension", "--extension", extensionPath],
|
||||
[cliPath, "--mode", "rpc", "--no-session", "--no-extensions", "--extension", extensionPath],
|
||||
{ stdio: ["pipe", "pipe", "pipe"] },
|
||||
);
|
||||
|
||||
@@ -518,9 +518,7 @@ async function main() {
|
||||
|
||||
// -- Process agent stdout --
|
||||
|
||||
const stdoutRl = readline.createInterface({ input: agent.stdout!, terminal: false });
|
||||
|
||||
stdoutRl.on("line", (line) => {
|
||||
function handleAgentLine(line: string): void {
|
||||
let data: Record<string, unknown>;
|
||||
try {
|
||||
data = JSON.parse(line);
|
||||
@@ -583,6 +581,28 @@ async function main() {
|
||||
tui.requestRender();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const stdoutDecoder = new StringDecoder("utf8");
|
||||
let stdoutBuffer = "";
|
||||
|
||||
agent.stdout!.on("data", (chunk: Buffer) => {
|
||||
stdoutBuffer += stdoutDecoder.write(chunk);
|
||||
while (true) {
|
||||
const newlineIndex = stdoutBuffer.indexOf("\n");
|
||||
if (newlineIndex === -1) break;
|
||||
|
||||
const line = stdoutBuffer.slice(0, newlineIndex);
|
||||
stdoutBuffer = stdoutBuffer.slice(newlineIndex + 1);
|
||||
handleAgentLine(line.endsWith("\r") ? line.slice(0, -1) : line);
|
||||
}
|
||||
});
|
||||
|
||||
agent.stdout!.on("end", () => {
|
||||
stdoutBuffer += stdoutDecoder.end();
|
||||
if (stdoutBuffer.length > 0) {
|
||||
handleAgentLine(stdoutBuffer.endsWith("\r") ? stdoutBuffer.slice(0, -1) : stdoutBuffer);
|
||||
}
|
||||
});
|
||||
|
||||
// -- User input --
|
||||
|
||||
61
third_party/pi/packages/coding-agent/examples/sdk/14-codemode-mcp.ts
vendored
Normal file
61
third_party/pi/packages/coding-agent/examples/sdk/14-codemode-mcp.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Codemode, Tool Search, and MCP
|
||||
*
|
||||
* The CLI loads `codemode`, `tool_search`, and MCP as built-in extensions. SDK sessions add
|
||||
* them to the resource loader's extension factories.
|
||||
*
|
||||
* `codemode` and `tool_search` are registered inactive. Enable them through the `defaultTools` setting
|
||||
* (`tools` would also restrict the session to the named tools, which hides MCP tools), or let the
|
||||
* MCP extension activate them: `codemode` for servers with `codemode` exposure, `tool_search` for
|
||||
* servers with `deferred` exposure.
|
||||
*/
|
||||
|
||||
import {
|
||||
createAgentSession,
|
||||
createCodemodeExtension,
|
||||
createMcpExtension,
|
||||
createToolSearchExtension,
|
||||
DefaultResourceLoader,
|
||||
getAgentDir,
|
||||
SessionManager,
|
||||
SettingsManager,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
const cwd = process.cwd();
|
||||
|
||||
const resourceLoader = new DefaultResourceLoader({
|
||||
cwd,
|
||||
agentDir: getAgentDir(),
|
||||
extensionFactories: [
|
||||
createCodemodeExtension({ mode: "on" }),
|
||||
createToolSearchExtension(),
|
||||
// Reads mcp.json from the agent directory and the trusted project, like the CLI.
|
||||
createMcpExtension(),
|
||||
],
|
||||
});
|
||||
await resourceLoader.reload();
|
||||
|
||||
const settingsManager = SettingsManager.create(cwd);
|
||||
// `+name` adds to the configured default tools instead of replacing them.
|
||||
settingsManager.applyOverrides({ defaultTools: ["+codemode", "+tool_search"] });
|
||||
|
||||
const { session } = await createAgentSession({
|
||||
resourceLoader,
|
||||
settingsManager,
|
||||
sessionManager: SessionManager.inMemory(),
|
||||
});
|
||||
|
||||
try {
|
||||
// Emits session_start, which connects the MCP servers in the background.
|
||||
await session.bindExtensions({});
|
||||
console.log("Active tools:", session.getActiveToolNames().join(", "));
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
|
||||
process.stdout.write(event.assistantMessageEvent.delta);
|
||||
}
|
||||
});
|
||||
await session.prompt("Use codemode to count the TypeScript files in src/ and list the three largest.");
|
||||
console.log();
|
||||
} finally {
|
||||
session.dispose();
|
||||
}
|
||||
@@ -15,18 +15,19 @@ The runtime example shows how to build a recreate function that closes over proc
|
||||
| `05-tools.ts` | Built-in tool allowlists |
|
||||
| `06-extensions.ts` | Logging, blocking, result modification |
|
||||
| `07-context-files.ts` | AGENTS.md context files |
|
||||
| `08-slash-commands.ts` | File-based slash commands |
|
||||
| `08-prompt-templates.ts` | File-based prompt templates |
|
||||
| `09-api-keys-and-oauth.ts` | API key resolution, OAuth config |
|
||||
| `10-settings.ts` | Override compaction, retry, terminal settings |
|
||||
| `11-sessions.ts` | In-memory, persistent, continue, list sessions |
|
||||
| `12-full-control.ts` | Replace everything, no discovery |
|
||||
| `13-session-runtime.ts` | Manage runtime-backed session replacement |
|
||||
| `14-codemode-mcp.ts` | Add the `codemode`, `tool_search`, and MCP extensions |
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cd packages/coding-agent
|
||||
npx tsx examples/sdk/01-minimal.ts
|
||||
node examples/sdk/01-minimal.ts
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
Reference in New Issue
Block a user