Agents & watchers
Register a coding agent with defineAgent and drive its status with a watcher.
Registering an agent makes it launchable from Pragma's new-tab menu, agent menus, the board, fanouts, and phones — with your icon, your models, and status reporting.
defineAgent
import { definePlugin, defineAgent } from "@pragma-sh/plugin";
const myAgent = defineAgent({
id: "my-agent",
name: "My Agent",
icon: AgentIcon,
iconPath: "./assets/agent.svg", // optional: browser URL, absolute path, or plugin-relative
launch: { command: ["my-agent", "chat"] },
models: async (ctx) => [{ id: "m-1", name: "Model 1" }],
// The first entry is the default every launch applies.
permissionModes: [
{ id: "auto", name: "Auto" },
{ id: "ask", name: "Ask before actions" },
],
modes: [
{ id: "build", name: "Build" },
{ id: "plan", name: "Plan" },
],
slashCommands: [{ name: "review", description: "Review changes", argumentHint: "[pr]" }],
args: {
model: (id) => ["--model", id],
reasoning: (id) => ["--reasoning", id],
permissionMode: (id) => (id === "auto" ? ["--yolo"] : []),
mode: (id) => ["--agent", id],
slashCommand: (name) => `/${name}`, // optional; this is the default
},
startupInput: [{ delayMs: 500, data: "/init\r" }],
prefillMode: "bracketed", // or "plain"
prefillSubmit: "\r",
prefillDelayMs: 200,
excludeFeatures: ["usageLimits"], // skip matching `agent verify` scenarios
});The args builders translate the user's selections (model, reasoning, mode, permission
mode) into launch arguments, appended in that order. startupInput writes keystrokes after the TUI starts;
prompt prefill sends the user's typed prompt and its submit key as two PTY writes
(bracketed mode waits for the terminal's alt screen, bounded by a generous timeout, so
agents that redraw themselves don't eat the prompt).
Session identity and resume
Pragma can bring an interrupted agent session back — after a crash, a server restart, or a Restart update — by resuming the harness's own conversation. Two plugin pieces make that possible: reporting the conversation id, and rebuilding the resume argv.
Report the conversation id. Call reportSessionId (SDK) or
pragma-cli agent report --agent <id> session-id --id … (CLI) whenever the tab's session is
created or switched:
import { reportSessionId } from "@pragma-sh/sdk";
await reportSessionId({ agent: agentId, id: harnessSessionId });The id is opaque to Pragma and never parsed — it must be exactly what your harness's resume
flag accepts. Report only the top-level conversation (never a subagent's), and report again
on an A→B→A switch. A report from a tracked launch automatically carries the host's run id
(PRAGMA_AGENT_RUN_ID) as agentRunId.
Declare the resume argv. Add launch.resume to the agent definition:
launch: {
command: ["my-agent"],
resume: ({ harnessSessionId, selection }) => [
"my-agent",
"--resume",
harnessSessionId,
// Replay the recorded selection: the host appends no model/mode args.
...(selection.modelId ? ["--model", selection.modelId] : []),
],
}The callback receives { harnessSessionId, selection, cwd } and returns the complete
argv. Unlike launch.command, the host appends no model, mode, permission-mode, or prompt
args, so the callback must replay selection itself. Pragma validates the result before
typing it into the shell: a non-empty string array, a non-empty executable, and no NUL/CR/LF
in any token. Resume is opt-in — a catalog agent without the callback reports
supportsResume: false and cannot be resumed.
Two rules keep a restore honest:
- The conversation, not the turn. Reopening a session never replays the original prompt,
slash command, startup input, or pre-launch (
!!) commands. - A failed resume is an error, never a fresh session. The record stays recoverable with its id and selection; Pragma surfaces the failure instead of opening a new conversation.
Limits worth knowing, all verified against the shipped integrations: OpenCode can only learn
the active conversation from user-initiated activity, so a TUI switch with no following
prompt is unobservable; Pi has no session id until its session file exists (after the first
assistant message); Prime Agent deliberately declares no resume because Pragma launches it
with --no-session. See the full support matrix in
Sessions & recovery.
Modes, permission modes, and slash commands
These three work like models: each is a static list or an async (ctx) => … provider,
and each has an args function that applies the selection.
| Field | Launcher UI | Applied by |
|---|---|---|
modes | Shift+Tab cycles them | args.mode(id) — e.g. opencode's --agent plan |
permissionModes | Dropdown beside the mode | args.permissionMode(id) |
slashCommands | Typing / opens a picker | Prefixes the prompt with args.slashCommand(name) |
The first mode and permission mode are the defaults: a launch that picks none (the
board, a phone, the SDK) still applies them. So a flag your agent always needs for
unattended runs — Claude Code's --permission-mode auto, Kimi's -y — belongs in the first
permission mode, not in launch.command, where no other selection could remove it.
Most agents read commands, skills, and agent profiles from markdown directories. The discovery helpers scan them on the host that owns the project (so remote projects work too), put your built-ins first, and fall back to the built-ins if the scan fails:
import { modeProvider, slashCommandProvider } from "@pragma-sh/plugin/catalog";
slashCommands: slashCommandProvider(
[{ name: "init", description: "Write AGENTS.md" }],
[
{ dir: ".my-agent/commands", layout: "files" }, // project, `a/b.md` → /a:b
{ dir: "~/.my-agent/skills", layout: "skills", prefix: "skill:" }, // <name>/SKILL.md
],
),
modes: modeProvider(
[{ id: "default", name: "Default" }],
[{ dir: ".my-agent/agents", layout: "files", nameFromFrontmatter: true }],
),Frontmatter description and argument-hint feed the picker; a skill with
user-invocable: false is skipped.
If your tool speaks the Agent Client Protocol, prefer its
own list — it already includes skills and custom commands. Pass how to start its ACP server
and Pragma reads the available_commands_update it sends after session/new:
slashCommands: slashCommandProvider(builtins, fallbackDirs, {
acp: { command: ["my-agent", "acp"] },
}),A command whose TUI syntax is not /<name> sets invocation (Codex skills use
{ name: "imagegen", invocation: "$imagegen" }).
Watchers — status and round trips
A watcher attaches to a launched session, reads its output, and reports status through the SDK:
import { definePlugin, defineWatcher } from "@pragma-sh/plugin";
import { reportStarted, reportAttention, reportStopped } from "@pragma-sh/sdk";
watchers: [
defineWatcher({
agent: "my-agent",
watch: async (ctx) => {
await reportStarted({ agent: ctx.agentId, client: ctx.sdk });
for await (const chunk of ctx.output) {
if (/awaiting your approval/.test(chunk)) {
await reportAttention({
agent: ctx.agentId,
kind: "command",
command: matchedCommand,
requestId,
client: ctx.sdk,
});
const approved = await ctx.sdk.agents.awaitDecision({
agent: ctx.agentId, requestId,
});
await ctx.sendKeys(approved ? "y\r" : "n\r");
}
}
await reportStopped({ agent: ctx.agentId, client: ctx.sdk });
},
}),
],WatcherContext gives you:
| Member | Meaning |
|---|---|
sdk | A ready PragmaClient with gateway credentials. |
agentId / config | The plugin-qualified agent id and your validated config. |
session | { id, tabId, worktreeId }. |
output | Async iterable of decoded terminal output chunks. |
sendKeys(data) | Write into the live session (answer prompts, press keys). |
reportMessage(msg) | Publish a rich agent message. |
signal | Aborts when the session exits or the watcher is stopped. |
The host supervises one pragma-watch sidecar per live session — it re-establishes
itself with fresh gateway credentials after a gateway restart and backs off on crash
loops. Interjections from the app and phones (AgentInput) are always delivered through
the watcher, because submit keys and timing are TUI-specific.
Verifying
pragma-cli agent verify --agent <your-id> runs the conformance suite — status
transitions, session naming, question forms, approvals, aborts, stream integrity, and a
well-formed slash-command list (--scenario slash-commands checks just that, without
launching anything).
Catalog excludeFeatures entries skip the scenario groups your agent genuinely lacks.
See CLI → Agents.