Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/docs/extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ An extension is a named bundle of what an agent runs with: tools, prompt section
| Field | Adds | See |
| --- | --- | --- |
| `tools` | Functions the model can call. | [Custom Tools](/agents/docs/custom-tools) |
| `sections` | Parts of the system prompt. | [Instructions](/agents/docs/instructions) |
| `sections` | Parts of the system prompt. | [System Prompt](/agents/docs/system-prompt) |
| `hooks` | Code that runs around model requests, tool calls, and compaction. | [Hooks](#hooks) |
| `wraps` | Decorators for a tool or section, by name. | [Wrap a tool](#wrap-a-tool) |
| `tasks` | Multi-step work that resumes after a crash. | [Tools and tasks](/agents/docs/pi#tools-and-tasks) |
Expand Down
7 changes: 5 additions & 2 deletions docs/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,11 @@ Each agent on Rivet is one [Rivet Actor](/actors/docs). You define the agent onc
## Concepts

<CardGroup>
<Card title="Instructions" href="/agents/docs/instructions">
Set an agent's system prompt and project rules, and add context to a single prompt.
<Card title="System Prompt" href="/agents/docs/system-prompt">
Build an agent's system prompt from sections, and load AGENTS.md from the workspace.
</Card>
<Card title="Skills" href="/agents/docs/skills">
Give an agent instructions for specific tasks that it loads only when a task needs them.
</Card>
<Card title="User Subscriptions" href="/agents/docs/user-subscriptions">
Let your users run agents on their own Claude or ChatGPT subscription.
Expand Down
3 changes: 1 addition & 2 deletions docs/content/docs/pi.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,7 @@ See [Architecture](/agents/docs/architecture).

## Known limitations

- **Skills aren't supported yet.** Put the skill files in the workspace and add a prompt section that tells the model where they are, so it reads the right one before acting. See [Instructions](/agents/docs/instructions).
- **Pi doesn't load `AGENTS.md` files.** Set `instructions` or add a prompt section. See [Instructions](/agents/docs/instructions).
- **No automatic `AGENTS.md` or skill discovery.** Load them with prompt sections. See [System Prompt](/agents/docs/system-prompt) and [Skills](/agents/docs/skills).
- **No shell without a sandbox.** `bash` needs a [sandbox](/agents/docs/sandboxes).
- **Plain tool calls aren't rerun after a crash.** Use a task or `replay: "safe"`.
- **A tool that ignores its abort signal blocks sleep.** An upgrade then waits up to `sleepGracePeriod` for it to finish.
Expand Down
41 changes: 41 additions & 0 deletions docs/content/docs/skills.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: "Skills"
description: "Give an agent instructions for specific tasks that it loads only when a task needs them."
---

A skill is a `SKILL.md` file with a `name` and a `description`, followed by instructions for one kind of task. The model sees every skill's name and description in the [system prompt](/agents/docs/system-prompt) and loads a skill's instructions only when a task matches, so an agent can have many skills without filling its context.

## Write a skill

Put each skill in its own folder as `skills/<name>/SKILL.md`:

```md
---
name: commit-style
description: How to write commit messages in this project.
---

Write commit messages in the imperative mood and keep the subject under 50 characters.
```

The model decides when to use a skill from its `description` alone, so say which tasks it covers.

## Load skills

Pi doesn't discover skill files on its own. This extension lists them in a prompt section and loads them with a tool:

<CodeGroup>
<CodeSnippet file="examples/docs/skills/skills.ts" title="skills.ts" />
<CodeSnippet file="examples/docs/skills/server.ts" title="server.ts" />
</CodeGroup>

- **The `skills` section** lists every skill's name and description. Like every [section](/agents/docs/system-prompt#sections-for-every-conversation), it renders before each model call.
- **The `skill` tool** returns a skill's instructions. It sets `replay: "safe"`, so a call cut off by a crash runs again.
- **Skills ship with your worker.** They're read when the worker starts, and a deploy updates them. They work with or without a sandbox, and the agent can't change them.
- **Skills can differ per agent.** Look them up from your database in the section and the tool instead of from files, and every agent gets its own list.

## Skills in the workspace

Skill files that are already in the workspace, such as `.agents/skills/` in a repository cloned into the [sandbox](/agents/docs/sandboxes), need no tool. Add a section that says where they are, and the model reads a skill's `SKILL.md` with Pi's `read` tool before it starts the task.

**Next:** [Extensions](/agents/docs/extensions), how to bundle tools, sections, and hooks and choose them per conversation.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Instructions"
description: "Set an agent's system prompt and project rules, and add context to a single prompt."
title: "System Prompt"
description: "Build an agent's system prompt from sections, load AGENTS.md from the workspace, and add instructions to one conversation or context to one prompt."
---

An agent's system prompt is built from prompt sections. Your extensions add sections that every conversation gets, and a conversation can add its own `instructions` after them. The agent renders the system prompt again before every model call.
Expand Down Expand Up @@ -32,25 +32,39 @@ An agent's system prompt is built from prompt sections. Your extensions add sect

## Sections for every conversation

Add sections with an extension. Sections come in the order the extensions are installed.
Add sections with an extension. A section is a key and a function that returns its text:

<CodeSnippet file="examples/docs/instructions/server.ts" title="server.ts" />
<CodeSnippet file="examples/docs/system-prompt/server.ts" title="server.ts" />

The model gets the `payments-team` sections as tagged blocks:
The model gets each section as a block tagged with its key:

```text
<role>
You review pull requests for the payments team. Be brief and cite file paths.
</role>
<project-rules>
<date>
Today is Mon Oct 12 2026.
</date>
<agents_md>
Run `npm test` before proposing a fix. Never edit files in `migrations/`.
</project-rules>
<skills>
The `skills/` folder has one folder per skill. Read a skill's SKILL.md before you start a task it covers.
</skills>
</agents_md>
```

The agent runs each section's function again before every model call. A section can return text that changes, such as the current date or a value from your database.
- **Sections render before every model call.** A section can return text that changes, such as the current date, a file, or a value from your database.
- **Order follows the conversation's extensions,** which is install order by default. See [Extensions](/agents/docs/extensions).
- **Return `undefined` to leave a section out.** Pass `{ tag: false }` as the third argument of `section()` to send the text without the tags.
- **A section that throws keeps its previous text.** The model keeps what it already had, and the error is reported.
- **`wrapSection`** changes a section from another extension by its key. See [Wrap a tool](/agents/docs/extensions#wrap-a-tool).

## AGENTS.md

Pi doesn't read `AGENTS.md` on its own. Load it with a section, as `agents_md` does above. The section gets the conversation's workspace as `input.env`, the same files Pi's `read` tool sees: the [sandbox](/agents/docs/sandboxes), or the files in the Actor's database when there is no sandbox. An edit to `AGENTS.md` reaches the model on its next call.

With a sandbox, reading the file connects the sandbox on the first model call instead of the first tool call.

## How changes reach the model

The agent records a change to the system prompt in the conversation, at the point where it changed. After a restart, and in a fork, the conversation has exactly the system prompt the model saw at each point. On models that accept system prompt changes in the middle of a conversation, only the change is sent, so the provider's prompt cache stays valid.

## Instructions for one conversation

Expand All @@ -60,11 +74,6 @@ Call `conversation.configure(id, { instructions })` to give one conversation its

Put context for a single turn in the prompt itself. The `prompt` action takes a string, or an array of text and image blocks. Each image is `{ type: "image", data, mimeType }`, with the image base64-encoded in `data`. The system prompt stays the same.

<CodeSnippet file="examples/docs/instructions/client.ts" title="client.ts" />

## AGENTS.md and skills

Pi doesn't support `AGENTS.md` files or skills yet. Use these workarounds:
<CodeSnippet file="examples/docs/system-prompt/client.ts" title="client.ts" />

- **Project rules:** put the rules of your `AGENTS.md` in a section, as `project-rules` does above, or in a conversation's `instructions`.
- **Skills:** put the skill files in the agent's workspace, such as the [sandbox](/agents/docs/sandboxes) or the files in the Actor's database. Then add a section that tells the model where the skills are and to read one before it acts, as `skills` does above.
**Next:** [Skills](/agents/docs/skills), how to give an agent instructions it loads only when a task needs them.
8 changes: 6 additions & 2 deletions docs/sidebar.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@
"icon": "faRobot",
"pages": [
{
"title": "Instructions",
"href": "/agents/docs/instructions"
"title": "System Prompt",
"href": "/agents/docs/system-prompt"
},
{
"title": "Skills",
"href": "/agents/docs/skills"
},
{
"title": "Extensions",
Expand Down
20 changes: 20 additions & 0 deletions examples/docs/skills/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createRegistry } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
import { Skills } from "./skills";

const extensions = createRegistry();
extensions.install(CodingTools);
extensions.install(Skills);

const agent = pi({
model: "anthropic/claude-opus-5-5",
registry: extensions,
sandbox: e2bProvider(),
});

export const registry = setup({ use: { agent } });

registry.start();
42 changes: 42 additions & 0 deletions examples/docs/skills/skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { Type } from "@earendil-works/pi-ai";
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
import { defineExtension, defineTool, section } from "@earendil-works/pi-durable";

// One folder per skill, next to this file: skills/<name>/SKILL.md.
const root = join(import.meta.dirname, "skills");
const skills = new Map<string, { description: string; body: string }>();
for (const folder of await readdir(root, { withFileTypes: true })) {
if (!folder.isDirectory()) continue;
const file = await readFile(join(root, folder.name, "SKILL.md"), "utf8");
const { frontmatter, body } = parseFrontmatter<{ name?: string; description?: string }>(file);
skills.set(frontmatter.name ?? folder.name, { description: frontmatter.description ?? "", body });
}

const loadSkill = defineTool({
name: "skill",
description: "Load a skill's instructions by name.",
parameters: Type.Object({ name: Type.String() }),
// Reading a skill changes nothing, so running it twice is harmless.
replay: "safe",
execute: async ({ name }) => {
const skill = skills.get(name);
if (!skill) throw new Error(`No skill named ${name}. Skills: ${[...skills.keys()].join(", ")}.`);
return { content: [{ type: "text", text: skill.body }] };
},
});

export const Skills = defineExtension({
name: "skills",
tools: [loadSkill],
sections: [
// The model sees every skill's name and description. It loads a skill's instructions only when it needs them.
section("skills", () =>
[
"Skills hold instructions for specific tasks. Before you start a task that matches a skill's description, call the `skill` tool with its name and follow what it returns.",
...[...skills].map(([name, { description }]) => `- ${name}: ${description}`),
].join("\n"),
),
],
});
6 changes: 6 additions & 0 deletions examples/docs/skills/skills/commit-style/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
name: commit-style
description: How to write commit messages in this project.
---

Write commit messages in the imperative mood and keep the subject under 50 characters.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { posix } from "node:path";
import { createRegistry, defineExtension, section } from "@earendil-works/pi-durable";
import { CodingTools } from "@earendil-works/pi-durable/tools";
import { pi } from "@rivet-dev/pi";
Expand All @@ -11,9 +12,18 @@ extensions.install(
name: "payments-team",
sections: [
section("role", () => "You review pull requests for the payments team. Be brief and cite file paths."),
section("project-rules", () => "Run `npm test` before proposing a fix. Never edit files in `migrations/`."),
// The skill files are in the sandbox. This section tells the model where to find them.
section("skills", () => "The `skills/` folder has one folder per skill. Read a skill's SKILL.md before you start a task it covers."),
// Rendered again before every model call, so the date stays current.
section("date", () => `Today is ${new Date().toDateString()}.`),
// Read from the conversation's workspace, so an edit to AGENTS.md applies from the next model call.
section("agents_md", async (input, context) => {
if (!input.env) return undefined;
const file = await input.env.readTextFile(posix.join(input.env.cwd, "AGENTS.md"), context);
if (file.ok) return file.value;
// No AGENTS.md: leave the section out.
if (file.error.code === "not_found") return undefined;
// Any other failure keeps the text the model already has.
throw file.error;
}),
],
}),
);
Expand Down
Loading