@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.
Files changed (50) hide show
  1. package/package.json +14 -6
  2. package/service.js +18 -10
  3. package/src/agent/AgentLoop.js +2 -11
  4. package/src/agent/ContextAssembler.js +34 -3
  5. package/src/agent/Entries.js +16 -89
  6. package/src/agent/ProjectAgent.js +1 -16
  7. package/src/agent/TurnExecutor.js +12 -52
  8. package/src/agent/XmlParser.js +30 -117
  9. package/src/agent/errors.js +3 -22
  10. package/src/agent/materializeContext.js +3 -11
  11. package/src/hooks/Hooks.js +0 -29
  12. package/src/lib/hedberg/hedberg.js +4 -14
  13. package/src/lib/hedberg/marker.js +15 -59
  14. package/src/llm/LlmProvider.js +13 -26
  15. package/src/llm/errors.js +3 -11
  16. package/src/llm/openaiStream.js +6 -46
  17. package/src/plugins/ask_user/ask_user.js +12 -17
  18. package/src/plugins/budget/README.md +46 -8
  19. package/src/plugins/budget/budget.js +23 -42
  20. package/src/plugins/cp/cp.js +28 -18
  21. package/src/plugins/env/env.js +11 -7
  22. package/src/plugins/error/error.js +8 -37
  23. package/src/plugins/get/get.js +42 -24
  24. package/src/plugins/google/google.js +23 -3
  25. package/src/plugins/helpers.js +34 -50
  26. package/src/plugins/instructions/README.md +2 -2
  27. package/src/plugins/instructions/instructions-user.md +1 -1
  28. package/src/plugins/instructions/instructions.js +19 -6
  29. package/src/plugins/known/known.js +1 -8
  30. package/src/plugins/log/log.js +15 -1
  31. package/src/plugins/mv/mv.js +29 -19
  32. package/src/plugins/persona/persona.js +4 -4
  33. package/src/plugins/prompt/README.md +1 -1
  34. package/src/plugins/prompt/prompt.js +1 -1
  35. package/src/plugins/rm/rm.js +26 -15
  36. package/src/plugins/rm/rmDoc.md +0 -2
  37. package/src/plugins/set/set.js +37 -84
  38. package/src/plugins/set/setDoc.md +16 -16
  39. package/src/plugins/sh/sh.js +10 -8
  40. package/src/plugins/skill/skillDoc.md +1 -1
  41. package/src/plugins/unknown/README.md +1 -1
  42. package/src/plugins/unknown/unknown.js +2 -6
  43. package/src/plugins/update/update.js +3 -2
  44. package/src/plugins/update/updateDoc.md +1 -1
  45. package/.env.example +0 -152
  46. package/.xai.key +0 -1
  47. package/PLUGINS.md +0 -962
  48. package/SPEC.md +0 -1897
  49. package/biome/no-fallbacks.grit +0 -50
  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`. |