Acme
AI Agent

Extending the agent

Add tools, adjust the system prompt, and teach the agent new workflows with skills.

The agent is designed to grow with your product. You can extend it in three main places:

  • Application tools that call your existing oRPC procedures
  • Native tools that run custom logic inside the agent
  • Skills that teach the agent reusable workflows

Add an application tool

Shared application tools live in packages/api/lib/tools/. Each tool wraps an oRPC procedure and is available to both the in-app agent and MCP clients.

The included list_notifications tool is a complete example:

{
	name: "list_notifications",
	label: "List notifications",
	description: "List the current user's recent notifications and their read state.",
	scope: "mcp:read",
	input: listNotificationsInput,
	output: listNotificationsOutput,
	procedure: listNotifications,
	executionMode: "sequential",
	invalidates: [],
}

To add your own tool:

  1. Write or reuse an oRPC procedure.
  2. Export its Zod schemas.
  3. Add a tool definition and register it in applicationTools.

Add a native tool

Native tools run directly inside the agent and do not require an API procedure. Define them under packages/agent/tools/ and include them in the agent's tool composition.

Chat displays each tool's name directly, such as web_search. Adding a tool does not require a tool-name translation.

For a ready-to-use example, enable the RAG tool to search an existing knowledge base with Pinecone, Weaviate, or Qdrant Cloud.

Web search is another native tool, enabled by default with keyless Exa. It includes Exa, Parallel, Firecrawl, and Tavily adapters using the same provider-selection pattern.

Image generation and editing use image_gen and image_edit, with private saved assets and five direct providers: OpenAI, Google, Black Forest Labs, xAI, and Ideogram.

import { defineTool } from "@repo/agent";
import { toolSchema } from "@repo/agent/tools";
import { z } from "zod";

const uppercase = defineTool({
	name: "uppercase",
	label: "Uppercase",
	description: "Convert text to uppercase",
	parameters: toolSchema(z.object({ text: z.string() })),
	async execute(_id, { text }) {
		return { content: [{ type: "text", text: text.toUpperCase() }], details: {} };
	},
});

Text-file handling

Text attachment policy lives in packages/agent/file-handling/text/config.ts. Edit extensions and filenames to allow common text formats or exact extensionless names such as Dockerfile, Makefile, README, and LICENSE. Matching ignores case and checks the final extension, not arbitrary filename prefixes. Browser MIME declarations do not determine eligibility.

The handler uses strict UTF-8 decoding without parsing JSON, executing code, or converting documents. UTF-8 byte-order marks are omitted from model text; original file bytes and line endings are preserved. Invalid UTF-8 is rejected. maxFiles bounds each submission and maxContextCharacters bounds aggregate decoded file text in each admission and active model context. Restart the API and refresh clients after changing policy.

Files are admitted through the existing send request and saved in the same private assets as images. The protected GET /api/agent/sessions/{sessionId}/files/{fileId} endpoint returns the original bytes as a download. It checks current ownership, login, organization membership, and the conversation's references; asset IDs do not grant access on their own. Responses disable shared caching and inline execution. session.ts resolves active file references into model-only text; chat history retains attachment metadata.

Change the system prompt

The default prompt is a plain Markdown file at packages/agent/prompts/system.md. Edit it to change the agent's role, tone, and ground rules.

It covers task completion, truthful tool outcomes, source-grounded answers, image/file handling, and concise responses. Keep product-specific context brief; put detailed workflows in skills and tool-specific instructions in tool descriptions. Restart the API after editing the prompt, or rebuild/redeploy for production.

Add a skill

Skills are reusable playbooks. Choose the folder based on who should receive them:

FolderEmbedded agentMCP
packages/skills/skills/YesYes
packages/agent/skills/YesNo

Each skill is a folder with a SKILL.md file. For example, create packages/agent/skills/support-workflow/SKILL.md for an agent-only workflow:

---
name: support-workflow
description: Guide the in-app assistant through product support requests.
---

Ask for the affected feature and the observed behavior before suggesting a fix.

The agent loads both folders, inlines the skill bodies into its system prompt, and caches them for the process lifetime. An empty agent-only folder is fine. When both roots contain the same skill name, the agent-only version takes precedence for the agent; MCP continues to receive the shared version. Use matching folder and frontmatter names. Keep bodies concise and self-contained: the embedded agent does not have Pi's built-in file-reading tools.

Restart the API after changing skills. The API build combines shared and agent-only skills in its own dist/skills/; the separate MCP build copies only the shared folder. Rebuild/redeploy each affected application for production changes.

Change the model

The package root keeps index.ts as its public entry point, models.ts for the conversational model catalog, and config.ts for feature settings. Supporting implementation files and their colocated tests live in packages/agent/lib/. The existing tools/, runtime/, file-handling/, prompts/, skills/, and integration tests/ folders remain directly under packages/agent/.

Edit packages/agent/models.ts and choose its defaultModel key. A catalog entry for Anthropic can use:

const model = {
	label: "Sonnet",
	provider: "anthropic",
	model: "claude-sonnet-4-5",
	apiKeyEnv: "ANTHROPIC_API_KEY",
};

Set that credential in the API environment. Enable modelSelector in packages/agent/config.ts to offer all configured entries; thinkingSelector is independent. Entries for the same provider share one credential-variable reference. Configuration is validated at startup and does not require a billing system.

Use the official Pi model catalog, model configuration guide, and provider authentication guide to find the appropriate identifiers and credentials. See model setup for the embedded runtime's configuration and custom-provider boundaries.

Restart/redeploy the API and refresh the page after changing the model. The agent UI shows the capabilities reported by the running model configuration.

Capabilities and send requests

agent.capabilities returns modelSelector, thinkingSelector, defaultModel, and models. Each model exposes id, label, thinkingLevels, defaultThinkingLevel, images, and contextWindow. Existing top-level thinking/image fields describe the default model for older clients. Tool discovery, image-generation settings, and text-file policy remain application-wide. Credential names and values are never included in the catalog response.

Send an optional modelId (the application key) and optional thinkingLevel with the ordinary admission. Omitted keys choose the default; unknown keys or non-default selections while the picker is disabled return MODEL_UNAVAILABLE inside the API error's agentCode. A new image sent to a text-only model returns MODEL_IMAGES. Both are rejected before admission or asset writes.

Each accepted receipt records an execution object with the resolved modelId, provider, model, and effective thinking level. Changing model or thinking requires a new admission ID. An unchanged accepted retry returns the original receipt, even if that model has since been removed or remapped. Older no-model requests and receipts keep their retry semantics. History remains readable when models are retired; restoration never automatically dispatches an unfinished run.

On this page