Plugin protocol

The JSON-RPC wire format behind the SDK — for writing Agentty plugins in any language.

This page describes API version 1 of the wire format. If you are writing in JavaScript, the Node.js SDK wraps all of it; read the quick start first either way.

Transport#

Agentty starts the plugin with the plugin folder as the working directory:

runtimeCommand
nodenode <main> — Node.js from the login shell PATH, Homebrew, Volta or nvm
pythonpython3 <main>
executable<main>

Messages are JSON-RPC 2.0 objects, one per line, UTF-8, on stdin (Agentty → plugin) and stdout (plugin → Agentty). Lines longer than 16 MB are rejected. Anything on stdout that is not JSON is logged and ignored; stderr goes to the plugin log.

When stdin closes or shutdown arrives, exit. A plugin still running 1.5 seconds after shutdown gets SIGTERM, and SIGKILL 1.5 seconds after that. When Agentty quits, both follow immediately.

Agentty → plugin#

MessageKindparams
initializerequest — answer it{ apiVersion, agentty: { version }, plugin: { id, name, version, dir, dataDir }, language, context }
command/executenotification{ command, args, context }
panel/open · panel/closenotification{ context }
ui/eventnotification{ element, event, value?, item?, action?, context }
context/changednotification{ context }
url/opennotification{ path, query, url, context }
shutdownnotification{}

initialize is sent first, followed immediately by whatever started the plugin — a command, the panel opening, or a link. Answer initialize with any result, for example {}.

Plugin → Agentty#

Send these as requests (with an id) to get a result or an error, or as notifications (no id) when you do not care.

MethodPermissionparamsResult
ui/setPanel{ tree }null
ui/showPanel{}null
ui/notify{ message, kind }info, success, warning, errornull
ui/setBadge{ text }, max 8 charactersnull
context/get{}The context
host/info{}{ version, apiVersion, language }
host/openUrl{ url } — http/httpsnull
host/revealPathworkspace.read{ path } — absolute, existingnull
prompt/injectprompt.inject{ text, title?, target?, paneId?, workspaceId?, agent?, cwd?, submit? }{ status: "asked" } or { status: "sent", paneId }
terminal/sendterminal.write{ paneId?, text, submit? } — focused pane without paneId{ paneId }
session/getsession.read{ paneId?, maxTurns? } — default 200, max 2000{ paneId, agent, sessionId, title, cwd, status, turnCount, turns }
workspace/listworkspace.read{}[{ id, name, cwd, active, panes }]

Agentty drops ui/notify calls arriving faster than one per 700 ms, answering them normally, and stops a plugin that sends more than 240 messages a second. Context fields are limited by the plugin's permissions.

Error codes#

CodeMeaning
-32601Unknown method
-32602Invalid parameters — bad UI tree, no such pane, …
-32001Permission missing, or blocked for a minute after a link reached the plugin
-32002Unavailable — no window open, no session yet

An example exchange#

text
→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"apiVersion":1,"plugin":{"id":"hello"},"language":"en","context":{}}}
→ {"jsonrpc":"2.0","method":"panel/open","params":{"context":{}}}
← {"jsonrpc":"2.0","id":1,"result":{}}
← {"jsonrpc":"2.0","id":1,"method":"ui/setPanel","params":{"tree":{"type":"column","children":[{"type":"button","id":"go","label":"Go"}]}}}
→ {"jsonrpc":"2.0","id":1,"result":null}
→ {"jsonrpc":"2.0","method":"ui/event","params":{"element":"go","event":"click","context":{}}}
← {"jsonrpc":"2.0","id":2,"method":"prompt/inject","params":{"text":"Hello","target":"ask"}}
→ {"jsonrpc":"2.0","id":2,"result":{"status":"asked"}}

is Agentty to plugin, is plugin to Agentty. Request ids are counted per direction.

UI tree#

Every node is an object with a type:

text
column   { children, gap? }                gap: none | small | medium | large
row      { children, gap?, wrap? }
section  { title, children }
text     { text, style? }                  style: body | title | muted | small | code | error | success
button   { id, label, icon?, variant?, disabled? }   variant: primary | secondary | ghost | danger
input    { id, placeholder?, value? }
list     { id, items, empty? }
         items: [{ id, title, subtitle?, detail?, icon?, tone?, actions?: [{ id, label?, icon?, tooltip? }] }]
choice   { id, options: [{ value, label }], value? }
toggle   { id, label, value? }
badge    { text, tone? }                   tone: neutral | info | success | warning | error
spinner  { text? }
divider  {}

Events: button sends click; input sends change and submit with value; list sends select with item, and row buttons send action with item and action; choice sends change with the option value; toggle sends change with the new boolean.

A list item's tone colors its icon, using the same values as badge.

Testing without Agentty#

The plugin is an ordinary program reading stdin and writing stdout, so you can drive it from a test: write an initialize request, then the notifications you want to exercise, and assert on the JSON the plugin writes back.