Recipes

The package: what is in the directory, and how it is read.

An agent recipe is a directory. package.json declares what the package provides, and everything else is ordinary files your team and your agents can both read. There is no build step, no install store, and no deployment descriptor.

my-agent/
  package.json          # declares everything below, under a "pi" block
  SYSTEM.md             # instructions every agent starts from
  agents/
    agent.yaml          # the default entry point
    reviewer.yaml       # run it directly, or let agent.yaml orchestrate it
  skills/               # procedures, loaded when a task calls for one
  prompts/              # named deliverable shapes, as slash commands
  extensions/           # tools you write in TypeScript
  python/               # an optional locked Python project
KeyDeclaresRead next
agentsOne YAML file per agent. At least one is requiredAgents
skillsSKILL.md procedures an agent may loadSkills and prompts
promptsPrompt templates, invoked as slash commandsSkills and prompts
extensionsTypeScript the package owns and every session loadsExtensions
mcpWhich MCP servers the package may use, and how much of eachMCP
runtimeLocked Python and approved system capabilities the host must provide; Node uses ordinary dependenciesDependencies below
package.json
{
  "name": "acme-research",
  "version": "1.2.0",
  "description": "Research an account and produce a sourced brief.",
  "pi": {
    "agents": ["agents/*.yaml"],
    "skills": ["skills/**/SKILL.md"],
    "prompts": ["prompts/*.md"],
    "extensions": ["extensions/*.ts"]
  }
}
  • Omit agents, skills or prompts to get the directory of that name. extensions and mcp are never found by convention, because executable code and capability are declared or absent.
  • A path can be a file, a directory or a glob, and every one you write has to match something.
  • Paths stay inside the package, including through symlinks.
  • Declaration order is preserved, with each glob's matches ordered lexically.
  • An explicit [] resolves nothing of that kind.
  • A typo inside pi is an error, not a warning. Outside it, normal npm rules apply.

Node dependencies

Recipe extensions and scripts use ordinary npm package metadata. Put modules needed while the agent runs in dependencies, install them locally with your package manager, and commit its lockfile. Do not put runtime imports in devDependencies.

package.json
{
  "name": "spreadsheet-agent",
  "type": "module",
  "dependencies": {
    "exceljs": "4.4.0",
    "zod": "4.3.6"
  },
  "pi": {
    "extensions": ["extensions/*.ts"]
  }
}

A host installs production dependencies for the recipe package and any nested package, such as a self-contained skill. Pi's own runtime packages are host-provided peers and should not be duplicated in dependencies.

Python and system requirements

A recipe can declare what its code needs without prescribing how a host builds its environment. pi.runtime.python points at a locked Python project in the package. pi.runtime.system.packages names versioned capabilities that a host has approved and provides.

package.json
{
  "name": "spreadsheet-agent",
  "pi": {
    "runtime": {
      "python": {
        "project": "python",
        "lockfile": "python/uv.lock",
        "version": ">=3.12,<3.15",
        "imports": ["pandas", "openpyxl"]
      },
      "system": {
        "packages": [
          { "id": "document.pdf-tools", "version": "1" }
        ]
      }
    }
  }
}
  • project is a recipe-relative directory containing pyproject.toml; lockfile is its committed uv.lock.
  • A capable host installs the frozen lock into a recipe-local environment before the first model call. It does not modify system Python or resolve an unlocked graph.
  • imports is an optional boot preflight. Use import names such as openpyxl, which may differ from distribution names.
  • System packages are versioned capability IDs, not apt, Homebrew or shell commands. Recipes cannot run privileged installers.
  • A host must satisfy every declaration or reject the recipe with an actionable error. Unsupported requirements never degrade silently.

The smallest recipe that runs

package.json
{
  "name": "my-agent",
  "pi": {}
}
agents/agent.yaml
name: agent                          # required
model:
  name: anthropic/claude-sonnet-4-6  # required; nothing else is

An empty pi block still finds agents/, so those two files resolve. name and model.name are the only required fields anywhere in the format.

Run it

In the terminal, Pi with one flag. Install the extension once, then point Pi at any recipe directory.

pi install npm:@introspection-ai/recipes   # once per machine

pi --recipe .                              # the agent named "agent"
pi --recipe . --agent reviewer             # or pick one by name

From your own process, the same two steps are two functions. handle.session is Pi's own AgentSession, so there is no second API to learn.

host.ts
import {
  createAgentSession,
  resolveRecipe,
} from "@introspection-ai/recipes";

const recipe = resolveRecipe({ recipeDir: "." });
const handle = await createAgentSession({ recipe, agentName: "agent" });

await handle.session.prompt("Research the Acme account");
await handle.dispose();

Everything on the following pages is authored the same way for both. See Running locally for credentials and connections, and the SDK reference for every option.

What happens when you launch

  1. The package.json#pi block resolves to concrete file paths.
  2. Every agent YAML is parsed, including each from: chain.
  3. One agent is selected, and its model, thinking level and tool allowlist are fixed.
  4. Model credentials resolve, or the launch fails.
  5. Declared MCP servers are bound from local or host configuration.
  6. Selected skills, package prompts and every declared extension load.
  7. Its subagents become the only names the agent tool will accept.

All seven happen before the first model call, so a broken recipe fails at launch rather than halfway through a task.

The default agent is the one named agent. If no agent has that name and the package declares exactly one, that one is selected. Otherwise the caller must choose, and launching without a name is an error rather than a guess.

Validation runs first

There is no separate validation step to remember. Every launch with --recipe validates the authored package, and diagnostics stop the session before any model or MCP call. Unknown keys inside pi structures and unknown keys in an agent YAML are errors, not warnings, so a typo cannot quietly do nothing.

One snapshot per run

Resolution produces one immutable snapshot, and the root session and every child it delegates to read from that same snapshot. Two consequences while you author: an inheritance mistake surfaces at launch rather than when a child runs, and editing a file mid-session changes nothing until you relaunch.