agent

Source: src/agent.ts

An agent is content: a system prompt, a model, a set of tools. It is declared as Markdown + frontmatter, following the pi convention.

This file knows nothing about sessions or workflows: it turns files into data. Bringing an agent to life is spawn()’s job.

Agent

type

export type Agent = {
	/** Unique name, and how every caller refers to it. Mandatory in the file. */
	name: string;
	/**
	 * What this agent is for, in one line. Mandatory in the file.
	 *
	 * Not decoration: `route` and `orchestrate` hand this text to a model to
	 * decide who does the work, so a vague description produces vague routing
	 * that no parser can repair.
	 */
	description: string;
	/** Markdown body, used verbatim as the system prompt. */
	systemPrompt: string;
	/** Allowed tools. Absent means the read-only default is applied at spawn. */
	tools?: string[];
	/**
	 * Skills this agent may load, by name - an allowlist, exactly like `tools`.
	 *
	 * Absent means none: a subagent is offered no skill it did not ask for.
	 * `resolveSkills` in `src/skills.ts` says where a name is looked up.
	 */
	skills?: string[];
	/** Model pattern, e.g. `"anthropic/claude-sonnet-5"`. Absent means pi's default. */
	model?: string;
	/** Default lifetime, in a workflow as in `spawn`. An explicit call always wins. */
	lifetime?: Lifetime;
	/**
	 * How many subagents this agent runs at once when it delegates.
	 *
	 * Only meaningful for an agent whose `tools:` names `subagent`. It is the
	 * agent's own business rather than the caller's: how wide a split is worth
	 * making depends on how the agent was told to think about its task, which is
	 * what its definition says.
	 */
	concurrency?: number;
	/**
	 * Default for "give this agent its own herdr split". An explicit call wins.
	 *
	 * Declaring it here is often what you want: a scout is worth watching every
	 * time, whoever calls it.
	 */
	openInHerdr?: boolean;
	/** Where the definition was found - a repository's agents are loaded only on request. */
	source: AgentSource;
	/** File path, or a free label for an agent built in memory. */
	filePath: string;
};

An agent: the “who”. Inert data, no state, no session.

AgentScope

type

export type AgentScope = "user" | "project" | "both";

Where to look for definitions. Defaults to "user" - see {@link loadAgents}.

AgentSource

type

export type AgentSource = "user" | "project" | "builtin";

Where an agent definition came from.

"builtin" is what this package ships. It is the lowest priority of the three: a "user" definition of the same name replaces it, and a "project" one replaces both.

findAgent

function

export function findAgent(agents: Agent[], name: string): Agent { /* … */ }

Looks up an agent by name, or throws.

An unknown agent name is a programming error, not a runtime failure: we do not want a failed Result several steps later because of a typo in a workflow.

Lifetime

type

export type Lifetime = "task" | "workflow" | "session";

Lifetime of a subagent - the central choice of this library.

  • "task": born and dies with each task. Minimal context, reproducible.

  • "workflow": lives for the duration of the workflow. Remembers iterations.

  • "session": lives as long as the pi session. Long memory, watch it.

LIFETIMES

const

export const LIFETIMES = ["task", "workflow", "session"] as const satisfies readonly Lifetime[];

Every {@link Lifetime}, for whoever validates one - the tool’s schema names them from here.

loadAgents

function

export function loadAgents(options: { cwd?: string; scope?: AgentScope; builtin?: boolean } = {}): Agent[] { /* … */ }

Discovers the available agents.

The scope defaults to "user", and that is not a detail: project agents (.pi/agents/) are repository-controlled content, hence third-party instructions. They are only loaded on explicit request.

Precedence runs from the least specific to the most: the shipped definitions first when builtin is set, then the user’s, then the repository’s. Whoever is closer to the work wins the name. builtin is off by default: a script that asks for “the user’s agents” must not be handed ours as well. The extension asks for them, because there it is the difference between working out of the box and not working at all.

Discovery happens on every call: editing a .md is enough to reload it.

loadAgentsFromDir

function

export function loadAgentsFromDir(dir: string, source: AgentSource): Agent[] { /* … */ }

Reads every .md in a directory.

A file that does not parse is dropped in silence - that is pi’s own behaviour for an agent, and we keep it. A missing or unreadable directory yields [].

parseAgent

function

export function parseAgent(content: string, filePath: string, source: AgentSource): Agent | undefined { /* … */ }

Parses an agent definition.

Returns undefined when the frontmatter is not valid YAML, or when name or description is missing: this is pi’s behaviour, a file that is not an agent is ignored silently. Kept separate from {@link loadAgents} so it stays testable without touching the disk.