This is the canonical guide for building agents on PilotSwarm when you are using the SDK directly.
For a complete worked example, see examples/devops-command-center. It includes root and child system agents, named agents, skills, a session policy, and worker-side mock tools.
If you only remember one thing, remember this:
- prompts live in plugin files
- tool handlers live in worker code
- sessions only reference tool names
Author agents in a plugin directory and load that directory from your worker.
plugin/
├── agents/
│ ├── default.agent.md
│ ├── planner.agent.md
│ └── researcher.agent.md
├── skills/
│ └── web-research/
│ ├── SKILL.md
│ └── tools.json
└── .mcp.json
default.agent.md is your app-wide overlay for every session on the worker. PilotSwarm layers it underneath the embedded framework base prompt.
This does not change your plugin file structure. You still author default.agent.md, named *.agent.md files, and skills/**/SKILL.md the same way. The runtime now maps those layers into the Copilot SDK's structured system-prompt sections instead of concatenating one large prompt blob.
---
name: default
description: Base instructions for all sessions.
---
# My App Default Agent
You are a helpful assistant running in PilotSwarm.
Always use `write_artifact` when you create a file the user should download, and include the returned `artifact://` link in your response. Upload existing files (builds, archives, binaries) with `fromFile` instead of inline content.
If you need to wait or poll, use the `wait` tool rather than bash sleep. For long waits, assume the next turn may resume on a different worker unless you intentionally pass `preserveWorkerAffinity: true` for worker-local work.
Use `store_fact`, `read_facts`, and `delete_fact` for durable structured state rather than hiding important facts only in chat history. Treat facts as the authoritative memory layer for anything important.Important behavior:
- the markdown body becomes the app-wide default layer for your sessions
- it is not a selectable agent
- it still applies even when another agent prompt is used
- it extends the embedded PilotSwarm framework instructions rather than replacing them
Named agents supply authored workflows users invoke with @name or that other
agents can start by name. Reusable methods belong in skills; executable
integrations belong in tools/MCP. Packages can ship those capabilities without
any named agents. Sessions can also consult an agent's instructions through
load_agent_guidelines, naming the source to the user, without launching it or
adopting its identity or permissions.
---
name: researcher
description: Researches topics and writes concise markdown summaries.
tools:
- web_fetch
- write_artifact
- read_artifact
- list_artifacts
---
# Researcher Agent
You gather information, summarize it clearly, and save substantial outputs as artifacts.
Prefer tables when comparing several results.How it works:
- YAML frontmatter becomes runtime metadata
- the markdown body becomes the agent prompt
toolslimits the tools this agent may use
PilotSwarm includes built-in facts tools on workers, and every agent session gets them automatically, including system agents. You may still list them in frontmatter if you want the dependency to be obvious in the agent file:
---
name: benchmark-analyst
description: Stores and compares benchmark findings.
tools:
- read_facts
- store_fact
- delete_fact
---Use facts for short structured memory, coordination state, baselines, checkpoints, and other data that should survive dehydration. Facts are session-scoped by default and are cleaned up automatically when the session is deleted. Set shared=true only for durable cross-session memory that should remain until explicitly deleted.
The worker must register any tool the agent is allowed to call.
import { PilotSwarmWorker, defineTool } from "pilotswarm-sdk";
const webFetch = defineTool("web_fetch", { /* ... */ });
const writeArtifact = defineTool("write_artifact", { /* ... */ });
const readArtifact = defineTool("read_artifact", { /* ... */ });
const worker = new PilotSwarmWorker({
store: process.env.DATABASE_URL!,
githubToken: process.env.GITHUB_TOKEN!,
pluginDirs: ["./plugin"],
});
worker.registerTools([webFetch, writeArtifact, exportArtifact]);
await worker.start();If the tool is not registered on the worker, listing it in an agent file is not enough.
Use the same write_artifact surface for both text and binary outputs.
- For text the agent is authoring, the inline
contentflow is unchanged. - For files that already exist on the worker (builds, archives, any binary), pass
fromFile: "<path>"— bytes stream server-side and never transit the model. Small binaries can still be written inline withcontentTypeplusencoding: "base64". - To adopt another session's artifact, pass
fromArtifact: { sessionId, filename, expectedSha256? }for a server-side copy. - Every write returns
sha256and theartifact://link — include the link in the agent's response for user-facing handoff. - Consumers use
read_artifactwithtoFilefor large/binary artifacts,metaOnly: trueto verify provenance, or bounded inline content for text. - In the browser portal, non-text artifacts are download-only; they do not render inline as markdown previews.
This keeps builder-facing artifact workflows consistent across SDK, TUI, and portal hosts.
Skills are shared domain knowledge bundles.
---
name: web-research
description: Research workflow guidance for browsing and summarizing sources.
---
When researching:
1. Start from primary sources when possible.
2. Record exact URLs.
3. Save long outputs as markdown artifacts.Optional tools.json can declare the tools that skill expects:
{
"tools": ["web_fetch", "write_artifact", "read_artifact"]
}Use skills when several agents should share the same operating guidance.
System agents are long-lived background agents started or spawned by the runtime.
Example:
---
name: sweeper
description: Cleans up old sessions.
system: true
id: sweeper
parent: pilotswarm
title: Sweeper Agent
initialPrompt: >
You are now online. Start your cleanup loop and report summary status.
---
# Sweeper Agent
You clean up stale sessions and report cluster hygiene.Use system agents only when you want durable background behavior. Most apps only need named agents.
initialPrompt for a system agent is bootstrap startup content. It is sent automatically when the session is created, but it should not be treated as an ordinary user-authored chat line in the CLI/TUI.
const session = await client.createSession({
model: "github:claude-opus-4.6",
toolNames: ["web_fetch", "write_artifact", "read_artifact"],
});
await session.sendAndWait("@researcher Find the top 5 announcements from this week and save them as a report.");The worker supplies the actual agent definitions and tool handlers. The client only needs the serializable session config.
- body becomes the app-wide default prompt layer
- not selectable
- not a tool filter
- should contain app-wide rules you always want
- is wrapped beneath the embedded PilotSwarm framework base prompt
- is applied through SDK prompt sections rather than raw string concatenation
- frontmatter declares metadata
- frontmatter should include
schemaVersion: 1and aversionstring; new agents should usually start atversion: 1.0.0; use schema 3 only withinitialRequiredToolwhen the first turn must execute one of the agent's declared tools - body is the agent prompt
toolsis a filter, not a tool implementation
- must be registered on the worker
- should have accurate descriptions and schemas
- should not rely on prompt text alone for critical correctness
For known named agents, use spawn_agent(agent_name="...").
When the right agent name is not already known, use ps_list_agents to inspect
static deployment and enabled published definitions visible to this session.
Match the job to a role's description, declared tools, skills and source access,
then pass the exact returned agent_name. Prefer a suitable specialist over a
generic child or native task; if none fits, choose the appropriate generic path.
required_tool is no longer a supported argument to spawn_agent. A stale call
returns an error with discovery guidance. The named agent's existing
initialRequiredTool remains its startup requirement.
Do not pass package-owned tools to ad hoc children with tool_names.
Use task= to give either a named or custom child its assignment. Named children
load their own instructions and tools; omitting task uses their initialPrompt.
Do not use task="sweeper" or task="resourcemgr" as a substitute for selecting an
agent by name. Worker-managed system agents are not valid manual spawn targets.
If an agent wants to choose a different model for a sub-agent:
- call
list_available_models - use only an exact returned
provider:modelvalue - never guess or shorten the name
Use cron(seconds=N, reason="...") for fixed recurring intervals and cron_at(minute=M, hour=H, tz="Area/City", reason="...") for wall-clock schedules. Do not build wall-clock jobs as wake-and-check polling loops. When a cron wake-up resumes an agent, it should perform the scheduled work described by the reason before responding.
Use the named contract argument on spawn_agent; there is no separate contract tool. Include it only when required outputs or wake policy matter:
spawn_agent(
task="Scan market data and store the durable finding",
contract={
"purpose": "Market scan",
"successCriteria": ["source-backed summary", "durable result fact"],
"expectedFacts": [{ "key": "result/market-scan", "required": true }],
"expectedArtifacts": [],
"validationMode": "warn",
"wakeOn": "material_change"
}
)
Use contract.wakeOn for every delegation: any for chatty work where every update matters, and material_change for finite task results and watcher changes. A finite child's ordinary final reply leaves it alive and idle, so validate the result and close it explicitly with complete_agent. Reserve completion for actual terminal lifecycle outcomes such as explicit completion, cancellation, failure, or a blocked verdict. Use message_agent(..., contract_patch={ wakeOn: "..." }) to adjust the policy later.
To redirect a child while its turn runs, call message_agent(..., deliver="now"): the text is steered into the running turn (session turn control), or queued for the next turn when the child has no running turn that takes steers. stop_agent_turn(agent_id) stops the child's current turn and keeps the child alive, unlike cancel_agent.
Use send_session_message(..., expects_response=true) to ask another session for help. The target session must call reply_session_message(request_id=..., session_id=<sender>, body=...); answering only in its own transcript does not deliver a response back to the sender.
Shared UI surfaces render cross-session requests and replies as dedicated transcript cards so both sender and receiver can see the durable request/response context.
- Putting tool names in an agent file but never registering the tool handler
- Treating
default.agent.mdlike a selectable agent - Assuming the client can execute tools
- Using
task=instead ofagent_name=for known named agents - Letting prompts carry critical correctness without runtime validation
- Treating a system agent's
initialPromptas user chat instead of startup/bootstrap behavior