Skip to main content
piloop has six extension points. Three need only configuration: a role, a harness and an MCP server. A trigger can be configuration (a sql query) or code in a package. A runtime is code in a package. The evaluate command is your own program.

Roles and triggers

A role is a table in piloop.toml: what the agent is told (its prompt), when it starts (the run queue, or its triggers), where it works (its runtime and working copy), how it works (harnesses, models, MCP servers, timeout, concurrency) and what it may file. The built-in worker, watchdog and research roles are the same tables with defaults. Roles describes each key and each built-in trigger. A role either does runs or sweeps. A role that does runs (runs = true, like the worker) takes ideas from the run queue, works in a new worktree at the base of its run, and its commits are scored and judged. A sweeping role (like the watchdog and research) starts on its triggers, works at the head or in the project checkout, and nothing it commits is scored.

Define a role

Add a table with a new name, a prompt file and at least one trigger. This role reads each new escalation in the inbox and writes a plan for it, one agent at a time, in the project checkout:
Then write the prompt and store the config:
The daemon now checks the query on each tick. When it returns a row, the daemon requests a planner agent, and piloop status lists it as planner#<worker id>. The agent signs its comments planner#<worker id>.

Define a role that does runs

Add a table with a new name and runs = true. The role takes the worker’s defaults, so every other key is optional. This role runs the ideas of the speed track with Claude, two at a time, and stops each agent after 10 minutes:
takes is an SQL condition on the ideas table, so it can select by any column: priority, track, family, harness, or a subquery. Without takes on the worker, the worker also runs the speed ideas that wait for a free sprinter slot. Write the prompt and store the config:
The daemon queues each speed idea for a sprinter run. The run’s agent signs sprinter#<worker id>, its brief has the idea and the starting point as a worker’s has, and its commits are scored and judged like a worker’s. Roles lists which rules apply to each role and which apply to every run.

Add a trigger or a role from a package

A Python package can add triggers and roles through two entry point groups:
  • piloop.triggers: each entry point names a subclass of piloop.triggers.Trigger. Its name is the kind. Its fields are its parameters. Its fire(tick) method returns a Fire (the reason, and how many agents) or None. tick gives it the session, the project, the role and the queries that the built-in triggers use (piloop.triggers.Tick).
  • piloop.roles: each entry point names a piloop.config.SweepRole or a piloop.config.WorkerRole (a role that does runs), the defaults of the role. Its name is the name of the role. piloop init stores the role in the project’s config, with the project’s [roles.<name>] table over the defaults.
piloop loads each group once per process. A name that a built-in trigger or role has already is an error.
Register both in the package’s pyproject.toml, and install the package in the environment of piloop:
Any project can then use { kind = "no_keep", hours = 12 } in the triggers of a role, and gets the auditor role at its next piloop init.

Runtimes

A runtime is where an agent process runs. The daemon depends only on the Runtime protocol in piloop/runtime/__init__.py: A runtime class also has two class attributes, which piloop reads before it constructs the runtime:
  • kind: the name of the runtime. It is the name of its entry point, the value of runtime in piloop.toml and the value of workers.runtime.
  • invocation: the argument list of a harness that the runtime starts. headless is the argv for a process without a terminal, with the first prompt in it. interactive is the argv for a session in a terminal, and the runtime types the first prompt after the start.
The constructor takes the project’s name and path. piloop has two runtimes: subprocess, the default (a background process that cannot receive messages), and herdr (a terminal pane that you can watch and type into). A project chooses its runtime with the runtime key of piloop.toml, and a role can choose another one with its own runtime key. piloop init refuses a name that no runtime has.

Register a runtime

A Python package adds a runtime with an entry point in the piloop.runtimes group. This runtime starts each agent as the subprocess runtime does, at a lower CPU priority:
Install the package in the environment of piloop. A project then sets runtime = "nice", or a role sets it in its own table. A name that a built-in runtime has already, a kind that is not the name of the entry point, or a class without an invocation is an error that names the entry point. piloop attach, the Herdr checks of piloop doctor and the tab column of piloop watch work with the herdr runtime only, because they call Herdr.

Harnesses

A harness is a coding agent CLI. A [harness.<name>] table in piloop.toml says how to start it: the executable, the argv for an interactive session (interactive, for Herdr) and for a background run (headless), and the startup dialogs to accept. piloop fills in placeholders such as {model}, {session_id}, {cwd} and {prompt}.
The built-in harnesses pi, claude and codex are definitions of the same kind, so a project can override any key of them. Two optional parts of a harness have their own registries in the code: the usage reader that reads the tokens from the harness’s session files (piloop/usage/), and the mcp renderer that writes the MCP servers in the harness’s own flags (piloop/mcp/). The configuration reference has every key.

MCP servers

An [mcp.<name>] table defines an MCP server once, and [roles.<name>] mcp gives it to the agents of a role. piloop writes it in the format of each harness, and passes API keys only as environment variables. See MCP servers.

The evaluate command

The evaluate command is the contract between piloop and your project. The daemon runs it in a clean worktree of each commit. The command must:
  • print one line METRIC <name>=<value> on stdout for each metric in piloop.toml;
  • exit with status 0, within evaluate_timeout seconds. Otherwise the candidate is a crash.
When the daemon sets PILOOP_UNITS_FILE, the command can also write the result of each unit (each example, record or file) to that path, so that the verdict can use a paired bootstrap. See The evaluate command and per-unit results.