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.

FieldLauncher UIApplied by
modesShift+Tab cycles themargs.mode(id) — e.g. opencode's --agent plan
permissionModesDropdown beside the modeargs.permissionMode(id)
slashCommandsTyping / opens a pickerPrefixes 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:

MemberMeaning
sdkA ready PragmaClient with gateway credentials.
agentId / configThe plugin-qualified agent id and your validated config.
session{ id, tabId, worktreeId }.
outputAsync iterable of decoded terminal output chunks.
sendKeys(data)Write into the live session (answer prompts, press keys).
reportMessage(msg)Publish a rich agent message.
signalAborts 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.

On this page