Skip to main content
Agent

System Prompt

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.

section()every conversationinstructionsone conversationConversationsystem promptModelevery call

Sections for every conversation

Add sections with an extension. A section is a key and a function that returns its text:

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";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";

const extensions = createRegistry();
extensions.install(CodingTools);
extensions.install(
	defineExtension({
		name: "payments-team",
		sections: [
			section("role", () => "You review pull requests for the payments team. Be brief and cite file paths."),
			// 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;
			}),
		],
	}),
);

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

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

registry.start();

The model gets each section as a block tagged with its key:

<role>
You review pull requests for the payments team. Be brief and cite file paths.
</role>
<date>
Today is Mon Oct 12 2026.
</date>
<agents_md>
Run `npm test` before proposing a fix. Never edit files in `migrations/`.
</agents_md>
  • 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.
  • 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.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, 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

Call conversation.configure(id, { instructions }) to give one conversation its own instructions. They go after every extension section, and they apply from the next model call. Pass instructions: null to remove them.

Context for one prompt

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.

import { readFile } from "node:fs/promises";
import { createClient } from "rivetkit/client";
import type { registry } from "./server";

const client = createClient<typeof registry>();
const agent = client.agent.getOrCreate(["payments", "pr-482"]);

// Instructions for this conversation only. They go after the extension sections.
const root = await agent.harness.root();
await agent.conversation.configure(root.id, {
	instructions: "This conversation is about pr-482 in https://github.com/acme/payments. Focus on the checkout flow.",
});

// Context for one prompt goes in the prompt itself, text and images.
const screenshot = await readFile("checkout-error.png");
const result = await agent.prompt([
	{ type: "text", text: "Clone the repository and check out pr-482. This error shows up at checkout after that PR. Which change causes it?" },
	{ type: "image", data: screenshot.toString("base64"), mimeType: "image/png" },
]);

console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`);

Next: Skills, how to give an agent instructions it loads only when a task needs them.