@possumtech/rummy 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -6
- package/service.js +18 -10
- package/src/agent/AgentLoop.js +2 -11
- package/src/agent/ContextAssembler.js +34 -3
- package/src/agent/Entries.js +16 -89
- package/src/agent/ProjectAgent.js +1 -16
- package/src/agent/TurnExecutor.js +12 -52
- package/src/agent/XmlParser.js +30 -117
- package/src/agent/errors.js +3 -22
- package/src/agent/materializeContext.js +3 -11
- package/src/hooks/Hooks.js +0 -29
- package/src/lib/hedberg/hedberg.js +4 -14
- package/src/lib/hedberg/marker.js +15 -59
- package/src/llm/LlmProvider.js +13 -26
- package/src/llm/errors.js +3 -11
- package/src/llm/openaiStream.js +6 -46
- package/src/plugins/ask_user/ask_user.js +12 -17
- package/src/plugins/budget/README.md +46 -8
- package/src/plugins/budget/budget.js +23 -42
- package/src/plugins/cp/cp.js +28 -18
- package/src/plugins/env/env.js +11 -7
- package/src/plugins/error/error.js +8 -37
- package/src/plugins/get/get.js +42 -24
- package/src/plugins/google/google.js +23 -3
- package/src/plugins/helpers.js +34 -50
- package/src/plugins/instructions/README.md +2 -2
- package/src/plugins/instructions/instructions-user.md +1 -1
- package/src/plugins/instructions/instructions.js +19 -6
- package/src/plugins/known/known.js +1 -8
- package/src/plugins/log/log.js +15 -1
- package/src/plugins/mv/mv.js +29 -19
- package/src/plugins/persona/persona.js +4 -4
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +1 -1
- package/src/plugins/rm/rm.js +26 -15
- package/src/plugins/rm/rmDoc.md +0 -2
- package/src/plugins/set/set.js +37 -84
- package/src/plugins/set/setDoc.md +16 -16
- package/src/plugins/sh/sh.js +10 -8
- package/src/plugins/skill/skillDoc.md +1 -1
- package/src/plugins/unknown/README.md +1 -1
- package/src/plugins/unknown/unknown.js +2 -6
- package/src/plugins/update/update.js +3 -2
- package/src/plugins/update/updateDoc.md +1 -1
- package/.env.example +0 -152
- package/.xai.key +0 -1
- package/PLUGINS.md +0 -962
- package/SPEC.md +0 -1897
- package/biome/no-fallbacks.grit +0 -50
- package/gemini.key +0 -1
package/PLUGINS.md
DELETED
|
@@ -1,962 +0,0 @@
|
|
|
1
|
-
# PLUGINS.md — Plugin Development Guide
|
|
2
|
-
|
|
3
|
-
Every `<tag>` the model sees is a plugin. Every scheme is registered by
|
|
4
|
-
its owner. Every operation — model, client, plugin — flows through the
|
|
5
|
-
same tool handler. Exceptions to that discipline must justify themselves
|
|
6
|
-
in the architecture spec (SPEC.md).
|
|
7
|
-
|
|
8
|
-
## Quickstart
|
|
9
|
-
|
|
10
|
-
A complete tool plugin in four parts: register, handle, render, document.
|
|
11
|
-
|
|
12
|
-
```js
|
|
13
|
-
// src/plugins/ping/ping.js
|
|
14
|
-
import docs from "./pingDoc.js";
|
|
15
|
-
|
|
16
|
-
export default class Ping {
|
|
17
|
-
#core;
|
|
18
|
-
|
|
19
|
-
constructor(core) {
|
|
20
|
-
this.#core = core;
|
|
21
|
-
core.ensureTool();
|
|
22
|
-
core.registerScheme({ category: "logging" });
|
|
23
|
-
core.on("handler", this.handler.bind(this));
|
|
24
|
-
core.on("visible", this.full.bind(this));
|
|
25
|
-
core.on("summarized", this.summary.bind(this));
|
|
26
|
-
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
27
|
-
docsMap.ping = docs;
|
|
28
|
-
return docsMap;
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async handler(entry, rummy) {
|
|
33
|
-
const now = new Date().toISOString();
|
|
34
|
-
await rummy.set({
|
|
35
|
-
path: entry.resultPath,
|
|
36
|
-
body: `pong ${now}`,
|
|
37
|
-
state: "resolved",
|
|
38
|
-
attributes: { path: entry.path },
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
full(entry) { return entry.body; }
|
|
43
|
-
summary(entry) { return ""; }
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
```js
|
|
48
|
-
// src/plugins/ping/pingDoc.js
|
|
49
|
-
const LINES = [
|
|
50
|
-
["## ping",
|
|
51
|
-
"Header — model sees this as the tool name"],
|
|
52
|
-
["<ping/>",
|
|
53
|
-
"Simplest invocation — no path, no body"],
|
|
54
|
-
["* Returns server timestamp",
|
|
55
|
-
"One-line description of what the tool does"],
|
|
56
|
-
];
|
|
57
|
-
export default LINES.map(([text]) => text).join("\n");
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Install external plugins via npm + env var:
|
|
61
|
-
|
|
62
|
-
```env
|
|
63
|
-
RUMMY_PLUGIN_PING=@myorg/rummy.ping
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Plugin Contract {#plugins_contract}
|
|
67
|
-
|
|
68
|
-
A plugin is a directory under `src/plugins/` containing a `.js` file
|
|
69
|
-
that exports a default class. The class name matches the file name.
|
|
70
|
-
The constructor receives `core` (a PluginContext) — the plugin's
|
|
71
|
-
complete interface with the system.
|
|
72
|
-
|
|
73
|
-
```js
|
|
74
|
-
export default class MyTool {
|
|
75
|
-
#core;
|
|
76
|
-
|
|
77
|
-
constructor(core) {
|
|
78
|
-
this.#core = core;
|
|
79
|
-
core.ensureTool();
|
|
80
|
-
core.registerScheme({ category: "logging" });
|
|
81
|
-
core.on("handler", this.handler.bind(this));
|
|
82
|
-
core.on("visible", this.full.bind(this));
|
|
83
|
-
core.on("summarized", this.summary.bind(this));
|
|
84
|
-
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
85
|
-
docsMap.mytool = docs;
|
|
86
|
-
return docsMap;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async handler(entry, rummy) {
|
|
91
|
-
// What the tool does (rummy is per-turn RummyContext)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
full(entry) { return entry.body; }
|
|
95
|
-
summary(entry) { return entry.body; }
|
|
96
|
-
}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
File naming: `src/plugins/mytool/mytool.js`. Class name = file name.
|
|
100
|
-
Tool docs: `src/plugins/mytool/mytoolDoc.js` (annotated line arrays).
|
|
101
|
-
|
|
102
|
-
External plugins install via npm and load via `RUMMY_PLUGIN_*` env vars:
|
|
103
|
-
|
|
104
|
-
```env
|
|
105
|
-
RUMMY_PLUGIN_WEB=@possumtech/rummy.web
|
|
106
|
-
RUMMY_PLUGIN_REPO=@possumtech/rummy.repo
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Unified API {#plugins_unified_api}
|
|
110
|
-
|
|
111
|
-
Three tiers share the tool vocabulary, but the invocation shape and
|
|
112
|
-
dispatch path differ.
|
|
113
|
-
|
|
114
|
-
```
|
|
115
|
-
Model: <rm path="file.txt"/> → { name: "rm", path: "file.txt" }
|
|
116
|
-
→ TurnExecutor.#record()
|
|
117
|
-
→ hooks.tools.dispatch("rm", entry, rummy)
|
|
118
|
-
Client: { method: "rm", params: {...} } → rpc.js #dispatchRm(...)
|
|
119
|
-
→ Entries.rm({...})
|
|
120
|
-
Plugin: rummy.rm(path) / rummy.set({...}) → Entries.set / Entries.rm
|
|
121
|
-
→ (Entries also fires entry events)
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
Three surfaces, one grammar (see [surfaces](SPEC.md#surfaces)). The model dispatches through
|
|
125
|
-
the handler chain (`TurnExecutor.#record()` → `hooks.tools.dispatch`
|
|
126
|
-
→ policy filter → turn-scoped recording → abort cascade → budget
|
|
127
|
-
lifecycle around it). The client primitives (`set`/`get`/`rm`/`cp`/
|
|
128
|
-
`mv`/`update` RPCs) talk directly to Entries — `writer: "client"`
|
|
129
|
-
on every call, permissions enforced per-scheme. Plugins use
|
|
130
|
-
RummyContext verbs; the `rummy.entries` accessor is a Proxy that
|
|
131
|
-
auto-binds `writer: rummy.writer` on every write, so a plugin writing
|
|
132
|
-
on behalf of the model gets `writer: "model"` without opt-in.
|
|
133
|
-
|
|
134
|
-
Plugin code wanting full handler semantics (policy filter, proposal
|
|
135
|
-
flow, turn recording) calls `hooks.tools.dispatch` directly instead
|
|
136
|
-
of going through a primitive.
|
|
137
|
-
|
|
138
|
-
Verb signatures vary. See [plugins_rummy_verbs](#plugins_rummy_verbs).
|
|
139
|
-
|
|
140
|
-
## Registration {#plugins_registration}
|
|
141
|
-
|
|
142
|
-
All registration happens in the constructor via `core.on()`,
|
|
143
|
-
`core.filter()`, `core.ensureTool()`, and `core.registerScheme()`.
|
|
144
|
-
|
|
145
|
-
### core.ensureTool() {#plugins_ensure_tool}
|
|
146
|
-
|
|
147
|
-
Declares this plugin as a model-facing tool. Required for the tool
|
|
148
|
-
to appear in the model's tool list. Called automatically by
|
|
149
|
-
`core.on("handler", ...)` but must be called explicitly for tools
|
|
150
|
-
without handlers (e.g., `update`, `unknown`).
|
|
151
|
-
|
|
152
|
-
### core.registerScheme(config?) {#plugins_register_scheme}
|
|
153
|
-
|
|
154
|
-
Registers this plugin's scheme in the database. Called once in the
|
|
155
|
-
constructor.
|
|
156
|
-
|
|
157
|
-
```js
|
|
158
|
-
core.registerScheme({
|
|
159
|
-
name: "mytool", // defaults to plugin name
|
|
160
|
-
modelVisible: 1, // 1 or 0 — appears in v_model_context
|
|
161
|
-
category: "logging", // "data" | "logging" | "unknown" | "prompt"
|
|
162
|
-
scope: "run", // "run" | "project" | "global" — default scope
|
|
163
|
-
writableBy: ["model", "plugin"], // subset of: system | plugin | client | model
|
|
164
|
-
});
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
All fields optional. `core.registerScheme()` with no args gives a
|
|
168
|
-
sensible result-type scheme (logging category, run scope, writable by
|
|
169
|
-
model + plugin).
|
|
170
|
-
|
|
171
|
-
`scope` determines where entries at this scheme land (see
|
|
172
|
-
[entries](SPEC.md#entries) / [physical_layout](SPEC.md#physical_layout)).
|
|
173
|
-
`writableBy` is enforced at `Entries.set` — writes from a writer
|
|
174
|
-
not in the list throw a typed `PermissionError` (importable from
|
|
175
|
-
`src/agent/errors.js`). The four writer tiers
|
|
176
|
-
(see [writer_tiers](SPEC.md#writer_tiers)) form
|
|
177
|
-
a strict hierarchy: **system > plugin > client > model**. Each tier
|
|
178
|
-
is a superset of what's below.
|
|
179
|
-
|
|
180
|
-
### core.on(event, callback, priority?) {#plugins_on}
|
|
181
|
-
|
|
182
|
-
| Event | Payload | Purpose |
|
|
183
|
-
|-------|---------|---------|
|
|
184
|
-
| `"handler"` | `(entry, rummy)` | Tool handler — called when model/client invokes this tool |
|
|
185
|
-
| `"visible"` | `(entry)` | Visible-visibility projection — body shown in `<visible>` (data) or `<log>` (logging) |
|
|
186
|
-
| `"summarized"` | `(entry)` | Summarized-visibility projection — path + summary only (body hidden) |
|
|
187
|
-
| `"turn.started"` | `({rummy, mode, prompt, loopIteration, isContinuation})` | Turn beginning — plugins write prompt/instructions entries |
|
|
188
|
-
| `"turn.response"` | `({rummy, turn, result, responseMessage, content, commands, ...})` | LLM responded — write audit entries, commit usage |
|
|
189
|
-
| `"proposal.prepare"` | `({rummy, recorded})` | Tool dispatched — materialize proposals (e.g. file edit 202 revisions) |
|
|
190
|
-
| `"proposal.pending"` | `({projectId, run, proposed})` | Proposal awaits client resolution |
|
|
191
|
-
| `"turn.completed"` | `(turnResult)` | Turn resolved — full turnResult |
|
|
192
|
-
| `"entry.created"` | `(entry)` | Entry created during dispatch |
|
|
193
|
-
| `"entry.changed"` | `({runId, path, changeType})` | Entry content, visibility, or status modified |
|
|
194
|
-
| `"run.state"` | `({projectId, run, turn, status, summary, history, unknowns, telemetry})` | Incremental client-facing state push (wire-layer `status` HTTP code stays; DB stores the 5-value state enum) |
|
|
195
|
-
| `"error.log"` | `({runId, turn, loopId, message})` | Runtime error — creates an `error://` entry |
|
|
196
|
-
| Any `"dotted.name"` | varies | Resolves to the matching hook in `src/hooks/Hooks.js` |
|
|
197
|
-
|
|
198
|
-
```js
|
|
199
|
-
// One-liner examples
|
|
200
|
-
core.on("handler", async (entry, rummy) => { /* tool logic */ });
|
|
201
|
-
core.on("visible", (entry) => entry.body);
|
|
202
|
-
core.on("summarized", (entry) => entry.attributes?.summary || "");
|
|
203
|
-
core.on("turn.started", async ({ rummy, mode }) => { /* write entries */ });
|
|
204
|
-
core.on("turn.response", async ({ rummy, result }) => { /* audit */ });
|
|
205
|
-
core.on("entry.changed", ({ runId, path, changeType }) => { /* react */ });
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### core.filter(name, callback, priority?) {#plugins_filter}
|
|
209
|
-
|
|
210
|
-
| Filter | Signature | Purpose |
|
|
211
|
-
|--------|-----------|---------|
|
|
212
|
-
| `"instructions.toolDocs"` | `(docsMap) → docsMap` | Add tool documentation (docsMap pattern) |
|
|
213
|
-
| `"assembly.system"` | `(content, ctx) → content` | Contribute to system message |
|
|
214
|
-
| `"assembly.user"` | `(content, ctx) → content` | Contribute to user message |
|
|
215
|
-
| `"llm.messages"` | `(messages) → messages` | Transform final messages before LLM call |
|
|
216
|
-
| `"llm.response"` | `(response) → response` | Transform LLM response |
|
|
217
|
-
| `"llm.reasoning"` | `(reasoning, {commands}) → reasoning` | Contribute to `reasoning_content` (the think plugin subscribes here to merge `<think>` tag bodies) |
|
|
218
|
-
| Any `"dotted.name"` | varies | Resolves to the matching filter in the hook tree |
|
|
219
|
-
|
|
220
|
-
```js
|
|
221
|
-
// One-liner examples
|
|
222
|
-
core.filter("assembly.system", async (content, ctx) => {
|
|
223
|
-
return `${content}\n<mytag>${myData}</mytag>`;
|
|
224
|
-
}, 400);
|
|
225
|
-
core.filter("assembly.user", async (content, ctx) => {
|
|
226
|
-
return `${content}\n<status>${myStatus}</status>`;
|
|
227
|
-
}, 150);
|
|
228
|
-
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
229
|
-
docsMap.mytool = docs;
|
|
230
|
-
return docsMap;
|
|
231
|
-
});
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
The `ctx` object passed to assembly filters:
|
|
235
|
-
|
|
236
|
-
```js
|
|
237
|
-
ctx = {
|
|
238
|
-
rows, // turn_context rows (materialized entries)
|
|
239
|
-
loopStartTurn, // First turn of current loop
|
|
240
|
-
type, // "ask" or "act"
|
|
241
|
-
toolSet, // Set<string> of active tool names for this loop
|
|
242
|
-
contextSize, // Model context window size
|
|
243
|
-
lastContextTokens, // Actual API tokens from the prior turn (0 on turn 1)
|
|
244
|
-
turn, // Current turn number
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
#### Filter Priority Bands {#plugins_filter_bands}
|
|
249
|
-
|
|
250
|
-
Filters run in ascending priority order. The packet renders in
|
|
251
|
-
top-to-bottom order matching that — lower priority appears earlier in
|
|
252
|
-
the message. Current `assembly.user` registrations:
|
|
253
|
-
|
|
254
|
-
| Priority | Block | Plugin | Mutates per turn? |
|
|
255
|
-
|---|---|---|---|
|
|
256
|
-
| 50 | `<summary>` | `known.js` | Slow — only on new entry |
|
|
257
|
-
| 75 | `<visible>` | `known.js` | Fast — on every promote/demote |
|
|
258
|
-
| 100 | `<log>` | `log.js` | Always — appends per action |
|
|
259
|
-
| 200 | `<unknowns>` | `unknown.js` | On unknown lifecycle |
|
|
260
|
-
| 250 | `<instructions>` | `instructions.js` | On phase transition |
|
|
261
|
-
| 275 | `<budget>` | `budget.js` | Every turn (live) |
|
|
262
|
-
| 300 | `<prompt>` | `prompt.js` | Stable within a loop |
|
|
263
|
-
|
|
264
|
-
**Recommended ranges for new plugins** (for cache-friendly placement
|
|
265
|
-
and predictable rendering position):
|
|
266
|
-
|
|
267
|
-
| Range | Position | Use for |
|
|
268
|
-
|---|---|---|
|
|
269
|
-
| `0–49` | Top of user | Reserved (stable identity-tier blocks above `<summary>`) |
|
|
270
|
-
| `50–99` | Codebase data surface | Don't add here — owned by `known.js` |
|
|
271
|
-
| `100–149` | History tier | Action history, timeline-style content |
|
|
272
|
-
| `150–199` | Open slot | Inter-history blocks (e.g. recent-decisions, tracked progress) |
|
|
273
|
-
| `200–249` | State tier | Model state (open questions, work-in-progress) |
|
|
274
|
-
| `250–299` | Phase + budget | Avoid; current phase / budget arithmetic owned here |
|
|
275
|
-
| `300–349` | Task | Reserved for prompt-tier content |
|
|
276
|
-
| `350–999` | Bottom | Append-after-prompt content (rare; usually wrong) |
|
|
277
|
-
|
|
278
|
-
Within a band, lower priority = renders higher. Pick the smallest
|
|
279
|
-
priority that lands you in the right band and leaves room above and
|
|
280
|
-
below.
|
|
281
|
-
|
|
282
|
-
`assembly.system` currently has no registrations — system message is
|
|
283
|
-
the static identity surface (instructions base + tool docs). Adding
|
|
284
|
-
to `assembly.system` invalidates the system-prefix cache on whatever
|
|
285
|
-
provider you target; reserve for content that's truly stable per-run.
|
|
286
|
-
|
|
287
|
-
### Tool Docs {#plugins_tool_docs}
|
|
288
|
-
|
|
289
|
-
Each tool plugin has a `*Doc.js` file with annotated line arrays.
|
|
290
|
-
Text goes to the model. Rationale stays in source. Registered via
|
|
291
|
-
the `instructions.toolDocs` filter using the docsMap pattern:
|
|
292
|
-
|
|
293
|
-
```js
|
|
294
|
-
import docs from "./mytoolDoc.js";
|
|
295
|
-
|
|
296
|
-
core.filter("instructions.toolDocs", async (docsMap) => {
|
|
297
|
-
docsMap.mytool = docs;
|
|
298
|
-
return docsMap;
|
|
299
|
-
});
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
The instructions plugin filters by the active tool set — tools
|
|
303
|
-
excluded by mode or flags are automatically omitted from the docs.
|
|
304
|
-
|
|
305
|
-
### handler(entry, rummy) {#plugins_handler}
|
|
306
|
-
|
|
307
|
-
The handler receives the parsed command entry and a per-turn
|
|
308
|
-
RummyContext:
|
|
309
|
-
|
|
310
|
-
```js
|
|
311
|
-
entry = {
|
|
312
|
-
scheme, // Tool name ("set", "get", "rm", etc.)
|
|
313
|
-
path, // Entry path ("set://src/app.js")
|
|
314
|
-
body, // Tag body text
|
|
315
|
-
attributes, // Parsed tag attributes
|
|
316
|
-
resultPath, // Where to write the result
|
|
317
|
-
}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Multiple handlers per scheme. Lower priority runs first. Return
|
|
321
|
-
`false` to stop the chain.
|
|
322
|
-
|
|
323
|
-
#### Reporting outcomes {#plugins_handler_outcomes}
|
|
324
|
-
|
|
325
|
-
**The action entry IS its outcome.** Your handler finalizes the action's
|
|
326
|
-
own log entry at `entry.resultPath`. Success and failure are two values
|
|
327
|
-
of the same shape — body, state, outcome. The model sees both through
|
|
328
|
-
the same channel under your tool's scheme:
|
|
329
|
-
|
|
330
|
-
```js
|
|
331
|
-
async handler(entry, rummy) {
|
|
332
|
-
const { entries: store, runId, turn, loopId } = rummy;
|
|
333
|
-
const result = await runMyTool(entry.attributes);
|
|
334
|
-
|
|
335
|
-
if (result.failed) {
|
|
336
|
-
await store.set({
|
|
337
|
-
runId, turn, loopId,
|
|
338
|
-
path: entry.resultPath,
|
|
339
|
-
body: result.failureMessage,
|
|
340
|
-
state: "failed",
|
|
341
|
-
outcome: result.label, // "not_found", "validation", etc.
|
|
342
|
-
});
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
await store.set({
|
|
347
|
-
runId, turn, loopId,
|
|
348
|
-
path: entry.resultPath,
|
|
349
|
-
body: result.output,
|
|
350
|
-
state: "resolved",
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
That's the whole failure-reporting surface. Body is the result on
|
|
356
|
-
success, the failure message on failure. State labels the verdict
|
|
357
|
-
(`resolved` / `failed`). Outcome is a short machine-readable label.
|
|
358
|
-
|
|
359
|
-
The framework reads the post-handler state of every recorded entry
|
|
360
|
-
each turn; any `state="failed"` result counts as a strike toward
|
|
361
|
-
`MAX_STRIKES`. You don't need to do anything else to make the strike
|
|
362
|
-
fire — write the entry's outcome and the framework follows.
|
|
363
|
-
|
|
364
|
-
You do **not** call `hooks.error.log.emit` from a tool handler. That
|
|
365
|
-
hook is reserved for the framework's actionless-failure cases (parser
|
|
366
|
-
warnings, dispatch crashes, runtime watchdog, budget overflow) — none
|
|
367
|
-
of which a third-party plugin should be writing.
|
|
368
|
-
|
|
369
|
-
If your handler throws, the framework catches and emits a status-500
|
|
370
|
-
error entry on your behalf. That's the one case where the framework
|
|
371
|
-
writes for you. Throw with intent; don't try-catch your own handler
|
|
372
|
-
just to avoid a stack trace.
|
|
373
|
-
|
|
374
|
-
See SPEC [failure_reporting](SPEC.md#failure_reporting) for the
|
|
375
|
-
full contract and the rationale.
|
|
376
|
-
|
|
377
|
-
### full(entry) / summary(entry) {#plugins_views}
|
|
378
|
-
|
|
379
|
-
Returns the string the model sees for this tool's entries at the
|
|
380
|
-
given visibility. Every tool MUST register `full`. `summary` is
|
|
381
|
-
optional — if unregistered, falls back to `attributes.tags`
|
|
382
|
-
(model-authored keyword description) or empty string.
|
|
383
|
-
|
|
384
|
-
At summary visibility, `attributes.tags` is prepended above the
|
|
385
|
-
plugin's summary output automatically by ToolRegistry.view().
|
|
386
|
-
|
|
387
|
-
## Two Objects {#plugins_two_objects}
|
|
388
|
-
|
|
389
|
-
Plugins interact with two objects at different scopes:
|
|
390
|
-
|
|
391
|
-
**PluginContext** (`core`) — startup-scoped. Created once per plugin.
|
|
392
|
-
Used for registration (`on()`, `filter()`, `registerScheme()`,
|
|
393
|
-
`ensureTool()`). Available as `this.#core` throughout the plugin's
|
|
394
|
-
lifetime.
|
|
395
|
-
|
|
396
|
-
**RummyContext** (`rummy`) — turn-scoped. Passed to handlers per
|
|
397
|
-
invocation. Has tool verbs, per-turn state, database access.
|
|
398
|
-
|
|
399
|
-
### Tool Verbs (on RummyContext) {#plugins_rummy_verbs}
|
|
400
|
-
|
|
401
|
-
Convenience wrappers that bind `runId`, `turn`, `loopId` from context
|
|
402
|
-
and delegate to Entries. Signatures vary per verb. For full
|
|
403
|
-
handler-chain semantics (policy filtering, proposal flow, abort
|
|
404
|
-
cascade), call `rummy.hooks.tools.dispatch(scheme, entry, rummy)`
|
|
405
|
-
instead.
|
|
406
|
-
|
|
407
|
-
| Method | Effect |
|
|
408
|
-
|--------|--------|
|
|
409
|
-
| `rummy.set({ path?, body?, state?, visibility?, outcome?, attributes? })` | Create/update entry. If `path` omitted, slugifies from body/summary. State defaults to `"resolved"`. |
|
|
410
|
-
| `rummy.get(path)` | Promote entries matching a pattern (default visibility `"visible"`). |
|
|
411
|
-
| `rummy.rm(path)` | Remove entry's view. |
|
|
412
|
-
| `rummy.mv(from, to)` | Rename entry. |
|
|
413
|
-
| `rummy.cp(from, to)` | Copy entry to a new path. |
|
|
414
|
-
| `rummy.update(body, { status?, attributes? })` | Write the once-per-turn lifecycle signal to `update://<slug>`. |
|
|
415
|
-
|
|
416
|
-
### Query Methods {#plugins_rummy_queries}
|
|
417
|
-
|
|
418
|
-
| Method | Returns |
|
|
419
|
-
|--------|---------|
|
|
420
|
-
| `rummy.getBody(path)` | Body text or null |
|
|
421
|
-
| `rummy.getState(path)` | Categorical state (`"proposed"` \| `"streaming"` \| `"resolved"` \| `"failed"` \| `"cancelled"`) or null |
|
|
422
|
-
| `rummy.getOutcome(path)` | Outcome string (populated when state ∈ {failed, cancelled}) or null |
|
|
423
|
-
| `rummy.getAttributes(path)` | Parsed attributes `{}` or null |
|
|
424
|
-
| `rummy.getEntry(path)` | First matching entry or null |
|
|
425
|
-
| `rummy.getEntries(pattern, bodyFilter?)` | Array of matching entries |
|
|
426
|
-
| `rummy.setAttributes(path, attrs)` | Merge attributes via json_patch |
|
|
427
|
-
| `rummy.entries.logPath(runId, turn, action, target)` | Build a `log://turn_N/<action>/<slug>` path, slugified + collision-safe |
|
|
428
|
-
| `rummy.entries.slugPath(runId, scheme, content, summary?)` | Build a `<scheme>://<slug>` path, slugified + collision-safe |
|
|
429
|
-
|
|
430
|
-
#### Path conventions {#plugins_path_conventions}
|
|
431
|
-
|
|
432
|
-
Entry paths are bounded by a hard `length(path) <= 2048` DB
|
|
433
|
-
CHECK constraint. In normal use, paths stay well under ~100 chars
|
|
434
|
-
because plugins build them via `logPath` / `slugPath`, which run the
|
|
435
|
-
target through `slugify` (80-char cap, `/` preserved as separator,
|
|
436
|
-
URL-encoded per segment) and append an integer tie-breaker on
|
|
437
|
-
collision (e.g. `log://turn_3/set/src/app.js_2`).
|
|
438
|
-
|
|
439
|
-
Plugin authors should pass any model-supplied target straight
|
|
440
|
-
through these helpers instead of stitching paths from the model's
|
|
441
|
-
raw input. The helpers absorb arbitrary target length and exotic
|
|
442
|
-
character composition without the caller having to defend against
|
|
443
|
-
either. The 2048 limit is the outer wall, not the working budget.
|
|
444
|
-
|
|
445
|
-
### Properties {#plugins_rummy_properties}
|
|
446
|
-
|
|
447
|
-
| Property | Type | Notes |
|
|
448
|
-
|----------|------|-------|
|
|
449
|
-
| `rummy.entries` | Entries proxy | Write calls auto-carry `writer: rummy.writer`. Read-through for reads + internal ops. |
|
|
450
|
-
| `rummy.db` | SqlRite db | Prefer `entries` for plugin-facing data access |
|
|
451
|
-
| `rummy.hooks` | Hook registry | |
|
|
452
|
-
| `rummy.runId` | number | Current run |
|
|
453
|
-
| `rummy.projectId` | number | |
|
|
454
|
-
| `rummy.sequence` | number | Current turn number |
|
|
455
|
-
| `rummy.loopId` / `rummy.turnId` | number | |
|
|
456
|
-
| `rummy.type` | `"ask"` \| `"act"` | Current mode |
|
|
457
|
-
| `rummy.toolSet` | Set<string> \| null | Active tool list for this loop |
|
|
458
|
-
| `rummy.contextSize` | number \| null | Model context window |
|
|
459
|
-
| `rummy.systemPrompt` / `rummy.loopPrompt` | string | |
|
|
460
|
-
| `rummy.noRepo` / `rummy.noInteraction` / `rummy.noWeb` | boolean | Loop flags |
|
|
461
|
-
| `rummy.writer` | `"system"` \| `"plugin"` \| `"client"` \| `"model"` | Default `"model"` in handler dispatch. The Proxy on `rummy.entries` binds this to every write for permission checks (see [writer_tiers](SPEC.md#writer_tiers)). |
|
|
462
|
-
|
|
463
|
-
## Tool Display Order {#plugins_display_order}
|
|
464
|
-
|
|
465
|
-
Tools are presented to the model in priority order:
|
|
466
|
-
gather → reason → act → communicate.
|
|
467
|
-
|
|
468
|
-
Defined in `ToolRegistry.TOOL_ORDER`. `resolveForLoop(mode, flags)`
|
|
469
|
-
handles all exclusions:
|
|
470
|
-
|
|
471
|
-
| Condition | Excludes |
|
|
472
|
-
|-----------|----------|
|
|
473
|
-
| `mode === "ask"` | `sh` |
|
|
474
|
-
| `noInteraction` flag | `ask_user` |
|
|
475
|
-
| `noWeb` flag | `search` |
|
|
476
|
-
| `noProposals` flag | `ask_user`, `env`, `sh` |
|
|
477
|
-
|
|
478
|
-
## Hedberg {#plugins_hedberg}
|
|
479
|
-
|
|
480
|
-
Hedberg has two faces. The implementation is a **library** at
|
|
481
|
-
`src/lib/hedberg/` — pattern matching, fuzzy literal replacement,
|
|
482
|
-
unified-diff generation. Internal plugins import these utilities
|
|
483
|
-
directly:
|
|
484
|
-
|
|
485
|
-
```js
|
|
486
|
-
import { hedmatch, hedsearch } from "../../lib/hedberg/patterns.js";
|
|
487
|
-
import Hedberg, { generatePatch } from "../../lib/hedberg/hedberg.js";
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
A thin **plugin shim** at `src/plugins/hedberg/` re-exposes the same
|
|
491
|
-
surface on `core.hooks.hedberg` for external plugins shipped in
|
|
492
|
-
separate packages (`rummy.repo`, `rummy.web`, etc.) that can't reach
|
|
493
|
-
into rummy/main's internals via direct import.
|
|
494
|
-
|
|
495
|
-
```js
|
|
496
|
-
const { match, search, replace, generatePatch } = core.hooks.hedberg;
|
|
497
|
-
```
|
|
498
|
-
|
|
499
|
-
| Method | Purpose |
|
|
500
|
-
|--------|---------|
|
|
501
|
-
| `match(pattern, string)` | Full-string pattern match (glob, regex, literal) |
|
|
502
|
-
| `search(pattern, string)` | Substring search |
|
|
503
|
-
| `replace(body, search, replacement)` | Fuzzy literal replacement (whitespace-tolerant) |
|
|
504
|
-
| `generatePatch(path, old, new)` | Generate unified diff |
|
|
505
|
-
|
|
506
|
-
Edit-shape parsing for `<set>` bodies (the `<<:::IDENT...:::IDENT`
|
|
507
|
-
marker family — see SPEC.md "Edit Syntax") lives in
|
|
508
|
-
`src/lib/hedberg/marker.js` and is invoked by the XmlParser at
|
|
509
|
-
`<set>` resolution time. It's not on `core.hooks.hedberg` because no
|
|
510
|
-
external plugin needs to re-parse model output.
|
|
511
|
-
|
|
512
|
-
**The split is intentional.** `src/lib/` is for stateless utility
|
|
513
|
-
modules anyone in the project can import. `src/plugins/` is for
|
|
514
|
-
contracts exposed via the hook system. Hedberg is one of the few
|
|
515
|
-
modules that has both shapes — same code, two access paths, one for
|
|
516
|
-
internal consumers and one for cross-package consumers.
|
|
517
|
-
|
|
518
|
-
## Events & Filters {#plugins_events_overview}
|
|
519
|
-
|
|
520
|
-
**Events** are fire-and-forget. All handlers run. Return values ignored.
|
|
521
|
-
**Filters** transform data through a chain. Lower priority runs first.
|
|
522
|
-
All hooks are async.
|
|
523
|
-
|
|
524
|
-
### Project Lifecycle {#plugins_project_lifecycle}
|
|
525
|
-
|
|
526
|
-
| Hook | Type | When |
|
|
527
|
-
|------|------|------|
|
|
528
|
-
| `project.init.started` | event | Before project DB upsert |
|
|
529
|
-
| `project.init.completed` | event | After project created |
|
|
530
|
-
|
|
531
|
-
### Run & Loop Lifecycle {#plugins_run_loop_lifecycle}
|
|
532
|
-
|
|
533
|
-
| Hook | Type | When |
|
|
534
|
-
|------|------|------|
|
|
535
|
-
| `run.created` | event | Run just created in DB |
|
|
536
|
-
| `ask.started` | event | Run requested in ask mode |
|
|
537
|
-
| `act.started` | event | Run requested in act mode |
|
|
538
|
-
| `loop.started` | event | Loop execution beginning |
|
|
539
|
-
| `run.config` | filter | Before run config applied |
|
|
540
|
-
| `run.progress` | event | Transient turn activity (`thinking` / `processing` / `retrying`) |
|
|
541
|
-
| `run.state` | event | Turn conclusion, per-command incremental, or terminal run close — full state snapshot (status, history, unknowns, telemetry) |
|
|
542
|
-
| `turn.verdict` | filter | Post-turn decision: continue / abandon / strike. Filter chain — multiple plugins (strike streak, cycle detect, stagnation today; future voters can join) each transform a verdict object. Initial value `{ continue: true }`; final value drives the loop's continue/abandon decision. |
|
|
543
|
-
| `run.step.completed` | event | Turn verdict resolved (post-healer, pre-close) |
|
|
544
|
-
| `loop.completed` | event | Loop exit — fires from `finally`, guaranteed on every exit path |
|
|
545
|
-
| `ask.completed` | event | Ask-mode run finished |
|
|
546
|
-
| `act.completed` | event | Act-mode run finished |
|
|
547
|
-
| `proposal.prepare` | event | Per recorded entry — plugins materialize proposals (e.g. set plugin turns search/replace revisions into 202 entries) |
|
|
548
|
-
| `proposal.pending` | event | A materialized proposal awaits client resolution |
|
|
549
|
-
|
|
550
|
-
### Turn Pipeline {#plugins_turn_pipeline}
|
|
551
|
-
|
|
552
|
-
Hooks fire in this order every turn. Type column legend:
|
|
553
|
-
**event** = fire-and-forget, all handlers run, no return value;
|
|
554
|
-
**filter** = chain transform, ordered by priority, return value carries forward;
|
|
555
|
-
**call** = direct named-method invocation on a specific plugin.
|
|
556
|
-
Exceptions for `call`-shaped hooks are documented under
|
|
557
|
-
[Architectural exceptions](#plugins_architectural_exceptions).
|
|
558
|
-
|
|
559
|
-
| # | Hook | Type | When |
|
|
560
|
-
|---|------|------|------|
|
|
561
|
-
| 1 | `turn.started` | event | Plugins write prompt/instructions entries |
|
|
562
|
-
| 2 | `instructions.resolveSystemPrompt` | call ⚠ | System prompt assembly — single-owner exception (cache stability) |
|
|
563
|
-
| 3 | `context.materialized` | event | turn_context populated from v_model_context |
|
|
564
|
-
| 4 | `assembly.system` | filter | Build system message from entries (called from inside `materializeContext`) |
|
|
565
|
-
| 5 | `assembly.user` | filter | Build user message (prompt plugin adds `<prompt tokensFree tokenUsage>`) |
|
|
566
|
-
| 6 | `turn.beforeDispatch` | filter | Measure assembled tokens; if over and turn 1, demote prompt, re-materialize, re-check; still over → 413. Filter chain on the dispatch packet `{ messages, rows, contextSize, lastPromptTokens, assembledTokens, ok, overflow }`. Budget participates here; future plugins may trim, re-order, or annotate via the same surface. `ok=false` short-circuits dispatch. |
|
|
567
|
-
| 7 | `llm.messages` | filter | Transform messages before LLM call |
|
|
568
|
-
| 8 | `llm.request.started` | event | LLM call about to fire |
|
|
569
|
-
| 9 | (LLM completion call) | — | Direct provider call. Errors caught: ContextExceededError → 413; TimeoutError/AbortError → 504 strike (unless drain). |
|
|
570
|
-
| 10 | `llm.response` | filter | Transform raw LLM response |
|
|
571
|
-
| 11 | `llm.request.completed` | event | LLM call finished |
|
|
572
|
-
| 12 | (XML parse + parser-warning emission) | — | Synchronous; warnings emitted via `error.log` with `soft: true` — recoverable, no strike |
|
|
573
|
-
| 13 | `llm.reasoning` | filter | Layer plugin reasoning contributions onto API-provided seed (used by `<think>` plugin to merge content-channel thinking into reasoning_content) |
|
|
574
|
-
| 14 | `turn.response` | event | Plugins write audit entries (telemetry) |
|
|
575
|
-
| 15 | `entry.recording` | filter | Per command, during `#record()`. Returning an entry with `state: "failed"` (or `"cancelled"`) rejects it. |
|
|
576
|
-
| 16 | Per recorded entry (sequential, abort-on-failure): | | |
|
|
577
|
-
| | `tool.before` | event | Before handler dispatch |
|
|
578
|
-
| | `tools.dispatch` | call (keyed) | Scheme's registered handler runs. Keyed dispatch is principled — multi-plugin contract by scheme name. |
|
|
579
|
-
| | `tool.after` | event | Handler finished |
|
|
580
|
-
| | `entry.created` | event | Entry written to store |
|
|
581
|
-
| | `run.state` | event | Incremental state push to connected clients |
|
|
582
|
-
| | `proposal.prepare` | event | This entry's dispatch may have created proposals (e.g. set → 202 revisions) |
|
|
583
|
-
| | `proposal.pending` | event | Per each materialized proposal — client is notified, dispatch awaits resolution |
|
|
584
|
-
| 17 | `turn.dispatched` | event | Post-dispatch cleanup. Budget subscribes for Turn Demotion (visibility=summarized on visible rows that overflow) + 413 `error://` emission via `hooks.error.log.emit`. Future plugins may subscribe for any post-dispatch concern. |
|
|
585
|
-
| 18 | `update.resolve` | call ⚠ | Update plugin classifies this turn's `<update>` (terminal/continuation, override-to-continuation if actions failed, heal from raw content if missing). Single-owner exception — synchronous return value (`{ summaryText, updateText }`) is load-bearing. |
|
|
586
|
-
| 19 | `turn.completed` | event | Turn fully resolved with final status |
|
|
587
|
-
|
|
588
|
-
**Legend:** ⚠ = load-bearing exception (kept by design, see below); ✗ = refactor candidate (ceremonial coupling).
|
|
589
|
-
|
|
590
|
-
### Architectural exceptions {#plugins_architectural_exceptions}
|
|
591
|
-
|
|
592
|
-
The plugin contract aims for **events for emit, filters for transform,
|
|
593
|
-
keyed dispatch for multi-plugin lookups by category**. Five points
|
|
594
|
-
intentionally deviate. They're documented here so they aren't
|
|
595
|
-
mistaken for ceremony and "fixed" in a way that breaks the
|
|
596
|
-
load-bearing reason.
|
|
597
|
-
|
|
598
|
-
**1. `instructions.resolveSystemPrompt(rummy)` — single-owner, cache-stable.**
|
|
599
|
-
The system prompt is deliberately not a filter chain. Multiple
|
|
600
|
-
participants would defeat prefix-cache reasoning ("Static base in
|
|
601
|
-
system, phase-specific in user," see AGENTS.md instruction
|
|
602
|
-
discipline). One plugin owns the surface; direct call enforces it.
|
|
603
|
-
|
|
604
|
-
**2. `update.resolve({ recorded, ... })` — single-owner with
|
|
605
|
-
synchronous return value.** Caller (`TurnExecutor`) needs
|
|
606
|
-
`{ summaryText, updateText }` back to drive the resolve callback.
|
|
607
|
-
Events emit but don't return; only the update plugin understands
|
|
608
|
-
terminal-vs-continuation status semantics. Filter-chain shape
|
|
609
|
-
would only have one element (still update), so the chain would be
|
|
610
|
-
ceremony.
|
|
611
|
-
|
|
612
|
-
**3. Static utility imports across plugins
|
|
613
|
-
(`Entries.scheme`, `Entries.normalizePath`, `countTokens`,
|
|
614
|
-
`stateToStatus`).** Pure stateless utilities. Routing through
|
|
615
|
-
hooks adds a ceremony layer for zero capability gain — these aren't
|
|
616
|
-
extension points; they're canonical implementations.
|
|
617
|
-
|
|
618
|
-
**4. Hedberg lib + thin plugin shim.** The library lives at
|
|
619
|
-
`src/lib/hedberg/` (pattern matching, sed parsing, merge handling).
|
|
620
|
-
A thin plugin shim at `src/plugins/hedberg/hedberg.js` re-exposes
|
|
621
|
-
the same surface on `core.hooks.hedberg` for external plugins
|
|
622
|
-
(rummy.repo, rummy.web) that can't reach into rummy/main's
|
|
623
|
-
internals via direct import. Internal plugins use direct imports
|
|
624
|
-
from `src/lib/hedberg/`; external plugins use the hook namespace.
|
|
625
|
-
See [Hedberg](#plugins_hedberg) for the API table.
|
|
626
|
-
|
|
627
|
-
**5. Transport plugins (`cli`, `rpc`).** These are *interface*
|
|
628
|
-
plugins, not action plugins. Their job is to bridge external
|
|
629
|
-
interfaces (stdin/stdout, WebSocket) to the agent. Direct imports
|
|
630
|
-
of `ProjectAgent` / `RummyContext` are what makes them transports;
|
|
631
|
-
fitting them into the action-plugin shape would require running
|
|
632
|
-
the agent over a back-channel to itself.
|
|
633
|
-
|
|
634
|
-
**Anything else that looks like a direct named call into a plugin
|
|
635
|
-
is a seam, not an exception** — see the ✗-marked entries in the
|
|
636
|
-
Turn Pipeline above. Refactor surface tracked in AGENTS.md "Now"
|
|
637
|
-
under Phase 2.
|
|
638
|
-
|
|
639
|
-
`entry.changed` fires asynchronously from mutation points — not
|
|
640
|
-
pipeline-ordered. Subscribe when you need to react to any entry
|
|
641
|
-
modification (used by budget remeasurement and file-on-disk detection).
|
|
642
|
-
|
|
643
|
-
### Entry Events {#plugins_entry_events}
|
|
644
|
-
|
|
645
|
-
| Hook | Type | When |
|
|
646
|
-
|------|------|------|
|
|
647
|
-
| `entry.recording` | filter | Before entry stored. Return `{ state: "failed", outcome }` to reject. |
|
|
648
|
-
| `entry.created` | event | New entry added during dispatch |
|
|
649
|
-
| `entry.changed` | event | Entry content, visibility, or state modified |
|
|
650
|
-
|
|
651
|
-
`entry.recording` is a filter — plugins can validate, transform, or
|
|
652
|
-
reject entries before they hit the store. Payload:
|
|
653
|
-
`{ scheme, path, body, attributes, state, outcome }`. Second arg is
|
|
654
|
-
a context bag: `{ store, runId, turn, loopId, mode }`. Return the
|
|
655
|
-
entry object (modified or not). Set `state: "failed"` with an
|
|
656
|
-
`outcome` string (e.g. `"permission"`, `"validation"`) to reject —
|
|
657
|
-
the policy plugin uses this pattern for ask-mode rejections.
|
|
658
|
-
|
|
659
|
-
`entry.changed` fires on any mutation to an existing entry — body
|
|
660
|
-
update, visibility change, state change, attribute update. Payload:
|
|
661
|
-
`{ runId, path, changeType }`. Subscribers include the budget plugin
|
|
662
|
-
(remeasure context) and the repo plugin (detect file changes on disk).
|
|
663
|
-
|
|
664
|
-
### Budget {#plugins_budget}
|
|
665
|
-
|
|
666
|
-
| Hook | Type | When |
|
|
667
|
-
|------|------|------|
|
|
668
|
-
| `turn.beforeDispatch` filter | subscriber | Pre-LLM ceiling check on the dispatch packet. On first-turn 413 → Prompt Demotion + re-check; sets `ok=false` + `overflow` to short-circuit dispatch. |
|
|
669
|
-
| `turn.dispatched` event | subscriber | Post-dispatch re-check. On 413 → Turn Demotion + 413 `error://` entry via `hooks.error.log.emit`. |
|
|
670
|
-
| `assembly.user` filter | subscriber | Renders `<budget>` table into the user message. |
|
|
671
|
-
|
|
672
|
-
The budget plugin measures tokens on the assembled messages — the
|
|
673
|
-
actual content being sent to the LLM. No estimates at the ceiling,
|
|
674
|
-
no SQL token sums. The assembled message IS the measurement. When
|
|
675
|
-
turn 2+ information is available, the pre-LLM check prefers the
|
|
676
|
-
actual API-reported token count (`turns.context_tokens` from the
|
|
677
|
-
prior turn) over re-measuring the assembled string.
|
|
678
|
-
|
|
679
|
-
**Use of the assembler.** Budget calls the context assembler in two
|
|
680
|
-
spots — these are projections, not orchestration leaks:
|
|
681
|
-
|
|
682
|
-
- **Pre-LLM Prompt Demotion (`turn.beforeDispatch`)** — when the
|
|
683
|
-
first-turn packet overflows, budget demotes the prompt entry in
|
|
684
|
-
the DB, swaps `body` from `vBody` to `sBody` on the local prompt
|
|
685
|
-
row, and re-runs `ContextAssembler.assembleFromTurnContext` on
|
|
686
|
-
the modified rows. No `materializeContext` round-trip — the row
|
|
687
|
-
already carries both projections.
|
|
688
|
-
- **Post-dispatch projection (`turn.dispatched`)** — budget re-runs
|
|
689
|
-
`materializeContext` to project the *next* turn's packet
|
|
690
|
-
(entries written during dispatch need projection through
|
|
691
|
-
`hooks.tools.view`). If predicted next packet overflows, budget
|
|
692
|
-
demotes now so next turn's enforce isn't stuck with only the
|
|
693
|
-
prompt-demotion lever. Cost projection is the budget plugin's
|
|
694
|
-
job; the assembler is the measurement instrument.
|
|
695
|
-
|
|
696
|
-
**DB tokens vs assembled tokens:** The `tokens` column on `entries`
|
|
697
|
-
is strictly for DISPLAY — showing token costs on entry tags in
|
|
698
|
-
`<summary>` / `<visible>` so the model can reason about entry
|
|
699
|
-
sizes. It is NEVER used for budget decisions. Budget math uses only
|
|
700
|
-
assembled message token counts. These are two separate numbers that
|
|
701
|
-
must never be conflated. See
|
|
702
|
-
[budget_enforcement](SPEC.md#budget_enforcement) for the three-measure table.
|
|
703
|
-
|
|
704
|
-
### Client Notifications {#plugins_client_notifications}
|
|
705
|
-
|
|
706
|
-
| Hook | Type | When |
|
|
707
|
-
|------|------|------|
|
|
708
|
-
| `ui.render` | event | Text for client display |
|
|
709
|
-
| `ui.notify` | event | Status notification |
|
|
710
|
-
|
|
711
|
-
## Entry Lifecycle {#plugins_entry_lifecycle}
|
|
712
|
-
|
|
713
|
-
Every entry follows the same lifecycle regardless of origin:
|
|
714
|
-
|
|
715
|
-
1. **Created** — `entries` row (content) + `run_views` row (per-run
|
|
716
|
-
projection) via the two-prep upsert flow (see [physical_layout](SPEC.md#physical_layout)).
|
|
717
|
-
2. **Dispatched** — tool handler chain executes.
|
|
718
|
-
3. **State set** — handler sets `state` (`"proposed"` \| `"streaming"`
|
|
719
|
-
\| `"resolved"` \| `"failed"` \| `"cancelled"`) + optional
|
|
720
|
-
`outcome` string on the `run_views` row. State is view-side; body
|
|
721
|
-
is content-side. (See [entries](SPEC.md#entries).)
|
|
722
|
-
4. **Materialized** — `v_model_context` joins entries + run_views,
|
|
723
|
-
projects into `turn_context`.
|
|
724
|
-
5. **Assembled** — filter chain renders into system/user messages.
|
|
725
|
-
Model-facing tags carry `status="NNN"` (HTTP code) via
|
|
726
|
-
`src/agent/httpStatus.js`'s state-to-HTTP mapping — the model's
|
|
727
|
-
vocabulary is HTTP; the DB is categorical.
|
|
728
|
-
6. **Visible** — model sees the entry in its context.
|
|
729
|
-
|
|
730
|
-
Entries at `visibility = 'archived'` skip steps 4–6 (invisible to
|
|
731
|
-
model, discoverable via pattern search). Entries at `visibility =
|
|
732
|
-
'summarized'` render with `attributes.tags` (model-authored keyword
|
|
733
|
-
description) prepended above the plugin's `summarized` view output —
|
|
734
|
-
the body is hidden; promoting with `<get>` brings it back.
|
|
735
|
-
|
|
736
|
-
**Per-plugin visibility projection reference.** Each plugin chooses
|
|
737
|
-
what its `visible` / `summarized` view hooks return. Renderers trust
|
|
738
|
-
the projected body — they do NOT re-check `entry.visibility`.
|
|
739
|
-
|
|
740
|
-
| Plugin | Category | `visible` body | `summarized` body | Notes |
|
|
741
|
-
|--------|----------|-----------------|----------------|-------|
|
|
742
|
-
| `known` | data | `entry.body` | `""` | Tag's `summary` attr carries the keywords at summarized visibility |
|
|
743
|
-
| `unknown` | unknown | `entry.body` | `""` | Same pattern as known |
|
|
744
|
-
| `prompt` | prompt | `entry.body` | 500-char truncation with `[truncated — promote to see the complete prompt]` marker | |
|
|
745
|
-
| `budget` | logging | `entry.body` | `entry.body` | Feedback signal — kept visible |
|
|
746
|
-
| `update` | logging | `# update\n${entry.body}` | same as visible | Already 80-char capped by tool doc rule |
|
|
747
|
-
| `get` / `set` / `rm` / `cp` / `mv` / `sh` / `env` / `search` | logging | result body | `""` | Just the self-closing tag at summarized |
|
|
748
|
-
| `skill` | data | `entry.body` | `""` | Same as known |
|
|
749
|
-
| `file` (bare paths) | data | `entry.body` | `""` | Same as known |
|
|
750
|
-
|
|
751
|
-
Plugins providing only a `visible` hook fall back to
|
|
752
|
-
`attributes.tags` (model-authored keyword description) at summarized;
|
|
753
|
-
the renderer inserts it automatically. Plugins providing neither
|
|
754
|
-
default to empty body — the tag still renders with its attributes so
|
|
755
|
-
the model can pattern-match the path.
|
|
756
|
-
|
|
757
|
-
### Streaming Entries {#plugins_streaming_entries}
|
|
758
|
-
|
|
759
|
-
Producers whose output arrives over time (shell commands, web fetches,
|
|
760
|
-
log tails, file watches) use the **streaming entry pattern**. The
|
|
761
|
-
lifecycle extends beyond 202→200:
|
|
762
|
-
|
|
763
|
-
```
|
|
764
|
-
state: "proposed" (user decision pending)
|
|
765
|
-
→ accept → state: "resolved" (log entry: action happened)
|
|
766
|
-
+ state: "streaming" data entries (one per channel, growing)
|
|
767
|
-
→ "resolved" / "failed" on completion
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
**Producer plugin contract:**
|
|
771
|
-
|
|
772
|
-
1. On dispatch, create a **proposal entry** at `{scheme}://turn_N/{slug}`
|
|
773
|
-
with `state: "proposed"`, category=logging. Body empty;
|
|
774
|
-
`tags=command` attr.
|
|
775
|
-
2. On user accept (client sends `set { state: "resolved" }` on the
|
|
776
|
-
proposal path), `AgentLoop.resolve()` transitions the proposal
|
|
777
|
-
entry to `state: "resolved"` (it becomes the **log entry**) and
|
|
778
|
-
creates **data entries** at `{path}_1`, `{path}_2`, etc. with
|
|
779
|
-
`state: "streaming"`, category=data, visibility=summarized, empty body.
|
|
780
|
-
3. Producer/client calls `stream { run, path, channel, chunk }` RPC
|
|
781
|
-
to append chunks to the appropriate channel.
|
|
782
|
-
4. When the producer is done, `stream/completed { run, path, exit_code? }`
|
|
783
|
-
transitions all `{path}_*` data entries to a terminal state
|
|
784
|
-
(`"resolved"` on exit_code=0 or omitted; `"failed"` with outcome
|
|
785
|
-
`"exit:N"` otherwise) and rewrites the log entry body with final
|
|
786
|
-
stats. For client-initiated cancellation, the client calls
|
|
787
|
-
`stream/aborted { run, path, reason? }` instead — transitions
|
|
788
|
-
channels to `state: "cancelled"` with outcome=reason.
|
|
789
|
-
|
|
790
|
-
**Channel numbering:** Unix file descriptor convention — `_1` is the
|
|
791
|
-
primary stream (stdout for shell, body for fetch, lines for tail);
|
|
792
|
-
`_2` is alternate/error (stderr, redirects, anomalies); `_3`+ for
|
|
793
|
-
additional producer-specific streams.
|
|
794
|
-
|
|
795
|
-
**The `stream` plugin** owns the RPC infrastructure. Producer plugins
|
|
796
|
-
only need to:
|
|
797
|
-
- Create the proposal entry on dispatch (status=202)
|
|
798
|
-
- Rely on `AgentLoop.resolve()` to create data channels on accept
|
|
799
|
-
- Let clients/external producers call `stream`, `stream/completed`,
|
|
800
|
-
and `stream/aborted`
|
|
801
|
-
|
|
802
|
-
No scheme registration or tooldoc for the stream plugin itself — it's
|
|
803
|
-
pure RPC plumbing shared across all streaming producers.
|
|
804
|
-
|
|
805
|
-
## Bundled Plugins
|
|
806
|
-
|
|
807
|
-
| Plugin | Type | Description |
|
|
808
|
-
|--------|------|-------------|
|
|
809
|
-
| `get` | Core tool | Load file/entry into context |
|
|
810
|
-
| `set` | Core tool | Edit file/entry, visibility control |
|
|
811
|
-
| `known` | Core tool + Assembly | Save knowledge; renders `<summary>` (priority 50) and `<visible>` (priority 75) for all category=data entries |
|
|
812
|
-
| `rm` | Core tool | Delete permanently |
|
|
813
|
-
| `mv` | Core tool | Move entry |
|
|
814
|
-
| `cp` | Core tool | Copy entry |
|
|
815
|
-
| `sh` | Core tool | Shell command (act mode only). Streaming producer — see [plugins_streaming_entries](#plugins_streaming_entries) |
|
|
816
|
-
| `env` | Core tool | Exploratory command. Streaming producer — see [plugins_streaming_entries](#plugins_streaming_entries) |
|
|
817
|
-
| `stream` | Internal | Generic streaming-entry RPC (`stream`, `stream/completed`, `stream/aborted`, `stream/cancel`) for sh/env and future producers |
|
|
818
|
-
| `ask_user` | Core tool | Ask the user |
|
|
819
|
-
| `search` | Core tool | Web search (via external plugin) |
|
|
820
|
-
| `update` | Structural | Status report + lifecycle signal. `status="200\|204\|422"` terminates; `status="102"` continues. Exposes `hooks.update.resolve` for TurnExecutor. |
|
|
821
|
-
| `unknown` | Structural + Assembly | Register unknowns, render `<unknowns>` (priority 150) |
|
|
822
|
-
| `log` | Assembly | Render `<log>` (priority 100) — all logging-category entries plus pre-latest prompts |
|
|
823
|
-
| `prompt` | Assembly | Render `<prompt tokensFree="N" tokenUsage="M">` (priority 30, front of user message) |
|
|
824
|
-
| `hedberg` | Utility | Pattern matching, interpretation, normalization |
|
|
825
|
-
| `instructions` | Internal | System prompt assembly (`instructions-system.md` + `[%TOOLS%]` + `[%TOOLDOCS%]` + persona); renders `<instructions>` (priority 165) from `instructions-user.md`; exposes `hooks.instructions.resolveSystemPrompt` |
|
|
826
|
-
| `file` | Internal | File entry projections and constraints (`scheme IS NULL`) |
|
|
827
|
-
| `rpc` | Internal | RPC method registration + tool-fallback dispatch |
|
|
828
|
-
| `telemetry` | Internal | Audit entries, usage stats, reasoning_content |
|
|
829
|
-
| `budget` | Internal | Context ceiling enforcement: Prompt Demotion (pre-LLM first-turn 413) + Turn Demotion (post-dispatch). Subscribes to `turn.beforeDispatch` (filter) + `turn.dispatched` (event) + `assembly.user` (filter, priority 175 — renders `<budget>`). |
|
|
830
|
-
| `policy` | Internal | Ask-mode per-invocation rejections via `entry.recording` filter |
|
|
831
|
-
| `error` | Internal | `error.log` hook → `error://` entries |
|
|
832
|
-
| `think` | Tool | Private reasoning tag; contributes to `reasoning_content` via the `llm.reasoning` filter |
|
|
833
|
-
| `openai` / `ollama` / `xai` / `openrouter` | LLM provider | Register with `hooks.llm.providers`; handle `{prefix}/...` model aliases. Silently inert if their env isn't configured. |
|
|
834
|
-
| `persona` | Internal | Renders the persona body inside the system prompt; default at `persona/default.md`. Run-attribute `persona` overrides per run (1:1, immutable for the run's lifetime). |
|
|
835
|
-
| `skill` | Internal | `<skill path="..."/>` tag handler + `skill://` scheme. Walks file/folder/`.zip` (local or URL); registers content under `skill://<name>/...`. |
|
|
836
|
-
|
|
837
|
-
## External Plugins
|
|
838
|
-
|
|
839
|
-
| Plugin | Package | Description |
|
|
840
|
-
|--------|---------|-------------|
|
|
841
|
-
| Repo | `@possumtech/rummy.repo` | Git-aware file scanning and symbol extraction |
|
|
842
|
-
| Web | `@possumtech/rummy.web` | Web search and URL fetching via searxng |
|
|
843
|
-
|
|
844
|
-
Loaded via `RUMMY_PLUGIN_*` env vars. External plugins have access
|
|
845
|
-
to the same PluginContext API as bundled plugins.
|
|
846
|
-
|
|
847
|
-
## RPC Methods {#plugins_rpc}
|
|
848
|
-
|
|
849
|
-
Client-facing JSON-RPC 2.0 over WebSocket. Protocol version **2.0.0**.
|
|
850
|
-
The client surface is a thin projection of the plugin API (SPEC §0.3):
|
|
851
|
-
the six primitives match the plugin's `rummy.set` / `rummy.get` / etc.
|
|
852
|
-
exactly, plus a connection handshake and a few config verbs.
|
|
853
|
-
|
|
854
|
-
### Wire Format {#plugins_rpc_wire_format}
|
|
855
|
-
|
|
856
|
-
```json
|
|
857
|
-
// Request
|
|
858
|
-
{ "jsonrpc": "2.0", "id": 1, "method": "set", "params": { "run": "my_run", "path": "known://fact", "body": "...", "state": "resolved" } }
|
|
859
|
-
|
|
860
|
-
// Success response
|
|
861
|
-
{ "jsonrpc": "2.0", "id": 1, "result": { "ok": true } }
|
|
862
|
-
|
|
863
|
-
// Error response
|
|
864
|
-
{ "jsonrpc": "2.0", "id": 1, "error": { "code": -32603, "message": "set: path is required" } }
|
|
865
|
-
|
|
866
|
-
// Notification (server → client, no id)
|
|
867
|
-
{ "jsonrpc": "2.0", "method": "run/state", "params": { "run": "my_run", "turn": 3, "status": 200, ... } }
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
### Connection Handshake {#plugins_rpc_handshake}
|
|
871
|
-
|
|
872
|
-
First call every client makes. Establishes project identity and
|
|
873
|
-
enforces protocol-version compatibility.
|
|
874
|
-
|
|
875
|
-
| Method | Params | Notes |
|
|
876
|
-
|--------|--------|-------|
|
|
877
|
-
| `rummy/hello` | `{ name, projectRoot, configPath?, clientVersion? }` | Returns `{ rummyVersion, projectId, projectRoot }`. Server rejects MAJOR mismatch with a protocol-mismatch error. |
|
|
878
|
-
|
|
879
|
-
### Primitives (see [primitives](SPEC.md#primitives)) {#plugins_rpc_primitives}
|
|
880
|
-
|
|
881
|
-
Six verbs. Object-args matching the entry grammar. Writer is fixed to
|
|
882
|
-
`"client"` server-side; permissions enforced per-scheme via the
|
|
883
|
-
scheme's `writable_by`.
|
|
884
|
-
|
|
885
|
-
| Method | Params | Notes |
|
|
886
|
-
|--------|--------|-------|
|
|
887
|
-
| `set` | `{ run, path, body?, state?, visibility?, outcome?, attributes?, append?, pattern?, bodyFilter? }` | Wide semantic: write content, change visibility/state, merge attributes, append (streaming), pattern update. Writing to `run://<alias>` starts or cancels a run (see §11.4). State transitions on proposed entries route through `AgentLoop.resolve()` for scheme-specific side effects. |
|
|
888
|
-
| `get` | `{ run, path, bodyFilter?, visibility? }` | Promote an entry (or pattern) to visible visibility. |
|
|
889
|
-
| `rm` | `{ run, path, bodyFilter? }` | Remove entry's view. |
|
|
890
|
-
| `cp` | `{ run, from, to, visibility? }` | Copy entry to new path. |
|
|
891
|
-
| `mv` | `{ run, from, to, visibility? }` | Rename entry. |
|
|
892
|
-
| `update` | `{ run, body, status?, attributes? }` | Write the once-per-turn lifecycle signal to `update://<slug>`. |
|
|
893
|
-
|
|
894
|
-
### Run Lifecycle via Primitives {#plugins_rpc_run_lifecycle}
|
|
895
|
-
|
|
896
|
-
Runs are addressable as `run://<alias>` entries (SPEC §0.5). The
|
|
897
|
-
client manipulates run lifecycle via ordinary `set` calls:
|
|
898
|
-
|
|
899
|
-
| Action | Call |
|
|
900
|
-
|--------|------|
|
|
901
|
-
| Start a run (named) | `set { path: "run://<alias>", body: <prompt>, attributes: { model, mode?, persona?, temperature?, contextLimit?, noRepo?, noInteraction?, noWeb?, noProposals? } }` |
|
|
902
|
-
| Start a run (anonymous) | `set { path: "run://", body: <prompt>, attributes: { model, ... } }` — server synthesizes alias as `${model}_${unixEpochMs}` and returns it in the response |
|
|
903
|
-
| Cancel a run | `set { path: "run://<alias>", state: "cancelled" }` |
|
|
904
|
-
| Inject continuation | `set { path: "run://<alias>", body: <message> }` on an existing run |
|
|
905
|
-
| Accept a proposal | `set { run, path: "<entry>", state: "resolved", body?: <output> }` |
|
|
906
|
-
| Reject a proposal | `set { run, path: "<entry>", state: "cancelled", body?: <reason> }` |
|
|
907
|
-
|
|
908
|
-
Starting a new run is fire-and-forget: server returns `{ ok: true, alias }`
|
|
909
|
-
immediately; client watches the run's state transitions via the
|
|
910
|
-
`run/state` notification (and the `run://` entry itself).
|
|
911
|
-
|
|
912
|
-
### Config & Query Methods {#plugins_rpc_queries}
|
|
913
|
-
|
|
914
|
-
Not every server capability fits the entry grammar. These are
|
|
915
|
-
dedicated verbs with 1:1 plugin-API equivalents.
|
|
916
|
-
|
|
917
|
-
| Method | Params | Notes |
|
|
918
|
-
|--------|--------|-------|
|
|
919
|
-
| `ping` | — | Liveness check |
|
|
920
|
-
| `discover` | — | Return the live RPC catalog |
|
|
921
|
-
| `getModels` / `addModel` / `removeModel` | (see rpc.js) | Model aliases |
|
|
922
|
-
| `getRuns` / `getRun` | `{ limit?, offset? }` / `{ run }` | Run listing and detail |
|
|
923
|
-
| `getEntries` | `{ run, pattern?, scheme?, state?, visibility?, bodyFilter? }` | Read-only entry query. Returns `[{path, scheme, state, visibility, attributes, turn, tokens}]`. No promotion side-effect. Pair with `get` primitive (which is a write verb). |
|
|
924
|
-
| `file/constraint` | `{ pattern, visibility }` | Project-scoped: set overlay. `visibility ∈ {active, readonly, ignore}`. Patterns can be globs. `readonly` is enforced on `set://` accept in `AgentLoop.resolve()`. |
|
|
925
|
-
| `file/drop` | `{ pattern }` | Project-scoped: remove overlay row. |
|
|
926
|
-
| `getConstraints` | — | Project-scoped: returns `[{pattern, visibility}]`. |
|
|
927
|
-
| `stream` / `stream/completed` / `stream/aborted` / `stream/cancel` | | Streaming RPC — see [plugins_streaming_entries](#plugins_streaming_entries) |
|
|
928
|
-
|
|
929
|
-
**Why file constraints are typed RPCs and not `set` entries:** they
|
|
930
|
-
are project-scoped (no `run`), persist across runs, and `readonly`
|
|
931
|
-
requires enforcement server-side on `set://` accept. Every `set`
|
|
932
|
-
primitive call requires a run alias; constraints don't have one. The
|
|
933
|
-
typed verbs match the capability's actual shape rather than contorting
|
|
934
|
-
the grammar.
|
|
935
|
-
|
|
936
|
-
### Notifications (server → client) {#plugins_rpc_notifications}
|
|
937
|
-
|
|
938
|
-
| Method | Purpose |
|
|
939
|
-
|--------|---------|
|
|
940
|
-
| `run/state` | Incremental state push per tool dispatch |
|
|
941
|
-
| `run/proposal` | A proposed entry awaits client resolution |
|
|
942
|
-
| `stream/cancelled` | Server-initiated streaming cancellation |
|
|
943
|
-
| `ui/render` | Streaming UI output |
|
|
944
|
-
| `ui/notify` | Toast notification |
|
|
945
|
-
|
|
946
|
-
### Retired Methods (2.0.0)
|
|
947
|
-
|
|
948
|
-
Protocol 1.x shipped many methods that collapsed into the primitive
|
|
949
|
-
grammar. Clients migrating from 1.x need to replace the following:
|
|
950
|
-
|
|
951
|
-
| 1.x method | Replacement |
|
|
952
|
-
|------------|-------------|
|
|
953
|
-
| `init` | `rummy/hello` |
|
|
954
|
-
| `ask` / `act` / `startRun` | `set { path: "run://<alias>", body: <prompt>, attributes: { model, mode, ... } }` |
|
|
955
|
-
| `run/resolve` | `set { run, path, state, body? }` |
|
|
956
|
-
| `run/abort` / `run/cancel` | `set { path: "run://<alias>", state: "cancelled" }` |
|
|
957
|
-
| `run/rename` | `mv { run, from: "run://<old>", to: "run://<new>" }` |
|
|
958
|
-
| `run/inject` | `set { path: "run://<alias>", body: <message> }` on an existing run |
|
|
959
|
-
| `run/config` | `set { path: "run://<alias>", attributes: { ... } }` |
|
|
960
|
-
| `store` (demote) | `set { run, path, visibility: "summarized", pattern: true }` |
|
|
961
|
-
| `getEntries` | Kept as §11.5 typed helper — now filter-capable (scheme/state/visibility). Pairs with the `get` write primitive. |
|
|
962
|
-
| `get { persist }` / `store { persist, clear, ignore }` (file constraints) | `file/constraint { pattern, visibility }` and `file/drop { pattern }`. Project-scoped helpers in §11.5 with real server enforcement for `readonly`. |
|