Orchestration and extensions¶
Capabilities that organise work: delegating to other agents, planning, loading know-how on demand, running things later, telling you about it, and connecting outside tools.
| Capability | Tools | Spec block |
|---|---|---|
| Subagents | delegate_task, check_task, cancel_task |
subagents |
| Planning | write_plan, read_plan, add_task, update_task_status, … |
none |
| Skills | list_skills, load_skill, list_skill_files, read_skill_file, create_skill, edit_skill |
skills |
| KeywordCurator | none: injects <relevance-hints> |
keywords |
| ScheduledTasks | schedule_task, list_scheduled_tasks, update_scheduled_task, remove_scheduled_task, run_scheduled_task |
none |
| Push | send_push |
none |
| MCP | whatever each server offers | mcp |
Subagents¶
An agent can hand a self-contained job to a named subagent: a separate agent with its own model, instructions and tools, run in a fresh context.
delegate_task(agent_name, task, background?)runs it. Foreground returns the full output.background=truereturns a task id at once, and the result arrives later as a message in the conversation, so the model needn't poll.check_task(task_id, offset?)returns status and a page of output.cancel_task(task_id)stops a running background task.
A subagent does not see the conversation. The task must carry everything
it needs, and it is told to prefer file paths over pasted content. Each gets
the same scoped file tools as its parent, and every result is saved as a
Markdown file under data/processes/<agent>/, so a finished task can be
re-read.
Defining one¶
A subagent is a spec in specs/sub_agents/, named in the parent's roster:
# specs/sub_agents/coding_subagent.yaml
name: coding_subagent
model: openrouter:deepseek/deepseek-v4-flash-0731
description: General coding sub-agent for small, well-scoped engineering tasks.
profile: coding # its tools: codebase map + scoped file tools
subagent:
name: coding
timeout_seconds: 300
usage_limits:
request_limit: 30
total_tokens_limit: 400000
background: required # allowed | required | never
instructions: |
...
# in the parent's spec
subagents:
agents: [writer_subagent, code_reviewer_subagent, coding_subagent]
max_concurrent: 5
subagent: key |
Meaning |
|---|---|
timeout_seconds |
Wall-clock limit for one run (default 300). |
usage_limits |
request_limit (default 30) and total_tokens_limit for one run. |
background |
allowed, required (only runs in the background, for long work), or never. |
paf ships four: writer, code_reviewer, coding and researcher. A leaf's
profile picks its tools from the leaf profiles (researcher: web and files;
coding: codebase map and files). Leaves never get persistence, memory,
summaries or subagents of their own, so delegation is one level deep.
Limits¶
- A subagent doesn't inherit the parent's guards. It is built from its own
spec and profile, so the parent's tool gating, shell command policy and
injection screening don't apply to it unless the leaf spec sets up its own.
Check what a leaf's profile and spec give it before adding one to the roster
of a locked-down agent.
securehas noSubagentscapability for this reason. - Approvals aren't routed from subagents. Sending a subagent's approval
requests to you is not built yet, so don't rely on an
approverule inside a leaf.
Planning¶
On a long task the model keeps a plan: a list of tasks with statuses, optionally with subtasks and dependencies.
write_planreplaces the plan,read_planshows it.add_task,update_task_status,update_task_statuses,remove_task.add_subtask,set_dependencyandget_available_tasksfor work with structure: a parent completes by itself when its last subtask does.
The current plan is shown back to the model with every request as a reminder
that sits after the cache breakpoint, so editing the plan doesn't invalidate the
prompt cache. Plans are saved per conversation at
data/plans/<conversation_id>/plan.json, which survives restarts. You can read
the plan with /plan in paf chat, and the web UI shows it in a sidebar.
Skills¶
A skill is a directory with a SKILL.md (a name, a description, and
instructions for doing one kind of task well) plus optional resources in
references/, scripts/, templates/ and assets/. Skills keep the system
prompt short: the model sees the list and loads only what the task needs.
list_skillsshows the library as a category tree with one-line descriptions.load_skillreads a skill's instructions;list_skill_filesandread_skill_filereach its resources.create_skillandedit_skilllet the model write and refine skills in the library, so know-how it works out can be kept.
skills:
libraries:
- ~/skills
- ~/projects/skill-library
Skills are indexed when the agent is built, so a skill added while it runs
appears after a restart. Because the model can write skills that later turns
load as instructions, point libraries only at directories you want it to
change.
KeywordCurator¶
Tells the model which skills and notes are relevant to what you just said, without a model call on the turn.
A background job reads your skills and vault notes and builds a vocabulary:
canonical entities and their aliases, each pointing at a source. Each turn, the
last couple of your messages are matched against it, and the best few matches
are injected as a short <relevance-hints> block of pointers, which the
model can follow with load_skill or read_note. No match means nothing is
injected.
keywords:
inject: true
top_k: 3
curate: true # opt in: build the vocabulary in the background
curate is off by default because a curation pass sends digests of your
skills and notes to the model provider. Matching and injection don't call a
model. Knobs are in the spec reference.
ScheduledTasks¶
The agent can put work on a schedule. A task belongs to the conversation that created it.
schedule_task(schedule, name, description, instructions | script, notify?)takes a cron expression (0 9 * * *is 9am daily) and exactly one of:instructions: an agent task. At each firing the agent runs a turn in the conversation and follows the instructions.script: a script task. A script runs with no model, and the agent is brought in depending onnotify.list_scheduled_tasks,update_scheduled_task,remove_scheduled_task, andrun_scheduled_taskto fire one now.
For script tasks, notify: always has the agent review every run. This is the
way to start, while you build trust in the script. notify: on_trigger brings
the agent in only when the script exits non-zero: a cheap check that costs
tokens only when it finds something. Pair it with Push: "check X, and
tell me only if Y".
Jobs live in data/scheduled_tasks/, with a run history beside them. The
scheduler runs inside the SSE server (paf-sse), so jobs fire only while it
is up. They are not available in a bare paf chat.
Safety¶
- Script tasks run on the host. Not in the sandbox, not through the shell command policy, with a 60-second limit. A script task is as powerful as the shell, and a model that can schedule one can run code on your machine later. Don't give a locked-down profile this capability.
- Nobody is attached when a task fires, so an agent task that reaches a call needing approval is refused rather than waiting.
Push¶
send_push(title, body, urgent?) sends a notification to your subscribed
devices (the web UI, over Web Push). The model is told to use it sparingly:
when you'd want to be interrupted, and not to repeat what it just said or report
that there's nothing to report. Tapping the notification opens the
conversation, and the link is built by the runtime, not chosen by the model.
The runtime enforces the sparing use: at most 10 per hour per agent, an identical title and body suppressed for 10 minutes, a title cut to 60 characters and a body to 200. Only pushes that reach a device count against the limit.
The tool exists only where delivery is wired, which means the SSE server with
VAPID keys set (scripts/gen_vapid.py --write); in paf chat the model is
never offered a tool that can't work.
MCP¶
Connects Model Context Protocol servers, so
their tools appear as the agent's own. Available in the demo profile.
mcp:
config: specs/mcp/demo_servers.json
The file is the Claude Desktop mcpServers format:
{
"mcpServers": {
"time": { "command": "uvx", "args": ["mcp-server-time@2026.10.19"] },
"deepwiki": { "type": "http", "url": "https://mcp.deepwiki.com/mcp" }
}
}
- Each server's tools are prefixed with its name.
${VAR}and${VAR:-default}in the file expand from the environment, so credentials stay in.env, not in the config.- Servers connect on first use. A missing config file is an error at build time.
An MCP server runs outside paf's sandbox and policies: a command server is a
process on your machine, and an http server sends what it's given to that
host. Add only servers you trust, and pin versions as the example does.