@possumtech/rummy 0.5.0 → 2.0.0

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 (153) hide show
  1. package/.env.example +21 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +850 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +6 -4
  11. package/service.js +50 -9
  12. package/src/agent/AgentLoop.js +460 -330
  13. package/src/agent/ContextAssembler.js +4 -4
  14. package/src/agent/Entries.js +655 -0
  15. package/src/agent/ProjectAgent.js +30 -18
  16. package/src/agent/TurnExecutor.js +229 -421
  17. package/src/agent/XmlParser.js +99 -33
  18. package/src/agent/budget.js +56 -0
  19. package/src/agent/errors.js +22 -0
  20. package/src/agent/httpStatus.js +39 -0
  21. package/src/agent/known_checks.sql +8 -4
  22. package/src/agent/known_queries.sql +9 -13
  23. package/src/agent/known_store.sql +275 -125
  24. package/src/agent/materializeContext.js +102 -0
  25. package/src/agent/runs.sql +10 -7
  26. package/src/agent/schemes.sql +14 -3
  27. package/src/agent/turns.sql +9 -9
  28. package/src/hooks/HookRegistry.js +6 -5
  29. package/src/hooks/Hooks.js +44 -3
  30. package/src/hooks/PluginContext.js +29 -21
  31. package/src/{server → hooks}/RpcRegistry.js +2 -1
  32. package/src/hooks/RummyContext.js +135 -35
  33. package/src/hooks/ToolRegistry.js +21 -16
  34. package/src/llm/LlmProvider.js +64 -90
  35. package/src/llm/errors.js +21 -0
  36. package/src/plugins/ask_user/README.md +1 -1
  37. package/src/plugins/ask_user/ask_user.js +37 -12
  38. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  39. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  40. package/src/plugins/budget/README.md +27 -25
  41. package/src/plugins/budget/budget.js +260 -88
  42. package/src/plugins/cp/README.md +2 -2
  43. package/src/plugins/cp/cp.js +29 -11
  44. package/src/plugins/cp/cpDoc.js +2 -15
  45. package/src/plugins/cp/cpDoc.md +7 -0
  46. package/src/plugins/engine/README.md +2 -2
  47. package/src/plugins/engine/engine.sql +4 -4
  48. package/src/plugins/engine/turn_context.sql +10 -10
  49. package/src/plugins/env/README.md +20 -5
  50. package/src/plugins/env/env.js +45 -6
  51. package/src/plugins/env/envDoc.js +2 -23
  52. package/src/plugins/env/envDoc.md +13 -0
  53. package/src/plugins/error/README.md +16 -0
  54. package/src/plugins/error/error.js +151 -0
  55. package/src/plugins/file/README.md +6 -6
  56. package/src/plugins/file/file.js +15 -2
  57. package/src/plugins/get/README.md +1 -1
  58. package/src/plugins/get/get.js +103 -48
  59. package/src/plugins/get/getDoc.js +2 -32
  60. package/src/plugins/get/getDoc.md +36 -0
  61. package/src/plugins/hedberg/README.md +1 -2
  62. package/src/plugins/hedberg/hedberg.js +8 -4
  63. package/src/plugins/hedberg/matcher.js +16 -17
  64. package/src/plugins/hedberg/normalize.js +0 -48
  65. package/src/plugins/helpers.js +42 -2
  66. package/src/plugins/index.js +146 -123
  67. package/src/plugins/instructions/README.md +35 -9
  68. package/src/plugins/instructions/instructions.js +122 -9
  69. package/src/plugins/instructions/instructions.md +25 -0
  70. package/src/plugins/instructions/instructions_104.md +7 -0
  71. package/src/plugins/instructions/instructions_105.md +46 -0
  72. package/src/plugins/instructions/instructions_106.md +0 -0
  73. package/src/plugins/instructions/instructions_107.md +0 -0
  74. package/src/plugins/instructions/instructions_108.md +8 -0
  75. package/src/plugins/instructions/protocol.js +12 -0
  76. package/src/plugins/known/README.md +2 -2
  77. package/src/plugins/known/known.js +67 -36
  78. package/src/plugins/known/knownDoc.js +2 -17
  79. package/src/plugins/known/knownDoc.md +8 -0
  80. package/src/plugins/log/README.md +48 -0
  81. package/src/plugins/log/log.js +109 -0
  82. package/src/plugins/mv/README.md +2 -2
  83. package/src/plugins/mv/mv.js +55 -22
  84. package/src/plugins/mv/mvDoc.js +2 -18
  85. package/src/plugins/mv/mvDoc.md +10 -0
  86. package/src/plugins/ollama/README.md +15 -0
  87. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  88. package/src/plugins/openai/README.md +17 -0
  89. package/src/plugins/openai/openai.js +120 -0
  90. package/src/plugins/openrouter/README.md +27 -0
  91. package/src/plugins/openrouter/openrouter.js +121 -0
  92. package/src/plugins/persona/README.md +20 -0
  93. package/src/plugins/persona/persona.js +9 -16
  94. package/src/plugins/policy/README.md +21 -0
  95. package/src/plugins/policy/policy.js +29 -14
  96. package/src/plugins/prompt/README.md +1 -1
  97. package/src/plugins/prompt/prompt.js +58 -16
  98. package/src/plugins/rm/README.md +1 -1
  99. package/src/plugins/rm/rm.js +56 -12
  100. package/src/plugins/rm/rmDoc.js +2 -20
  101. package/src/plugins/rm/rmDoc.md +13 -0
  102. package/src/plugins/rpc/README.md +2 -2
  103. package/src/plugins/rpc/rpc.js +515 -296
  104. package/src/plugins/set/README.md +1 -1
  105. package/src/plugins/set/set.js +318 -75
  106. package/src/plugins/set/setDoc.js +2 -35
  107. package/src/plugins/set/setDoc.md +22 -0
  108. package/src/plugins/sh/README.md +28 -5
  109. package/src/plugins/sh/sh.js +50 -6
  110. package/src/plugins/sh/shDoc.js +2 -23
  111. package/src/plugins/sh/shDoc.md +13 -0
  112. package/src/plugins/skill/README.md +23 -0
  113. package/src/plugins/skill/skill.js +14 -18
  114. package/src/plugins/stream/README.md +101 -0
  115. package/src/plugins/stream/stream.js +290 -0
  116. package/src/plugins/telemetry/README.md +1 -1
  117. package/src/plugins/telemetry/telemetry.js +129 -80
  118. package/src/plugins/think/README.md +1 -1
  119. package/src/plugins/think/think.js +12 -0
  120. package/src/plugins/think/thinkDoc.js +2 -15
  121. package/src/plugins/think/thinkDoc.md +7 -0
  122. package/src/plugins/unknown/README.md +3 -3
  123. package/src/plugins/unknown/unknown.js +47 -19
  124. package/src/plugins/unknown/unknownDoc.js +2 -21
  125. package/src/plugins/unknown/unknownDoc.md +11 -0
  126. package/src/plugins/update/README.md +1 -1
  127. package/src/plugins/update/update.js +67 -5
  128. package/src/plugins/update/updateDoc.js +2 -30
  129. package/src/plugins/update/updateDoc.md +8 -0
  130. package/src/plugins/xai/README.md +23 -0
  131. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  132. package/src/server/ClientConnection.js +64 -37
  133. package/src/server/SocketServer.js +23 -10
  134. package/src/server/protocol.js +11 -0
  135. package/src/sql/v_model_context.sql +27 -31
  136. package/src/sql/v_run_log.sql +9 -14
  137. package/EXCEPTIONS.md +0 -46
  138. package/FIDELITY_CONTRACT.md +0 -172
  139. package/src/agent/KnownStore.js +0 -337
  140. package/src/agent/ResponseHealer.js +0 -241
  141. package/src/llm/OpenAiClient.js +0 -100
  142. package/src/llm/OpenRouterClient.js +0 -100
  143. package/src/plugins/budget/recovery.js +0 -47
  144. package/src/plugins/instructions/preamble.md +0 -45
  145. package/src/plugins/performed/README.md +0 -15
  146. package/src/plugins/performed/performed.js +0 -45
  147. package/src/plugins/previous/README.md +0 -16
  148. package/src/plugins/previous/previous.js +0 -56
  149. package/src/plugins/progress/README.md +0 -16
  150. package/src/plugins/progress/progress.js +0 -43
  151. package/src/plugins/summarize/README.md +0 -19
  152. package/src/plugins/summarize/summarize.js +0 -32
  153. package/src/plugins/summarize/summarizeDoc.js +0 -27
@@ -2,6 +2,8 @@ import msg from "../../agent/messages.js";
2
2
  import RummyContext from "../../hooks/RummyContext.js";
3
3
  import File from "../file/file.js";
4
4
 
5
+ const CONSTRAINT_VISIBILITIES = new Set(["active", "readonly", "ignore"]);
6
+
5
7
  export default class Rpc {
6
8
  #core;
7
9
 
@@ -22,21 +24,176 @@ export default class Rpc {
22
24
  description: "Returns { methods, notifications } catalog.",
23
25
  });
24
26
 
25
- r.register("init", {
27
+ // --- Primitives (SPEC primitives) ---
28
+ // The client surface is a thin projection of the plugin API.
29
+ // Six verbs, each takes an object of entry-grammar params.
30
+ // Writer is fixed to "client"; permissions enforced per scheme.
31
+
32
+ r.register("set", {
33
+ handler: async (params, ctx) => {
34
+ return await this.#dispatchSet(params, ctx);
35
+ },
36
+ description:
37
+ "Create or update an entry. Wide semantic: write content, change " +
38
+ "visibility/state, merge attributes, append (streaming), pattern update. " +
39
+ "Writing to run://<alias> starts or cancels a run.",
40
+ params: {
41
+ run: "string — run alias (except for new run:// writes, where the alias is in the path)",
42
+ path: "string — entry path (e.g. known://fact or run://abc)",
43
+ body: "string? — entry body",
44
+ state: "string? — proposed | streaming | resolved | failed | cancelled",
45
+ visibility: "string? — visible | summarized | archived",
46
+ outcome: "string? — reason when state ∈ {failed, cancelled}",
47
+ attributes: "object? — JSON attributes",
48
+ append: "boolean? — append body rather than overwrite",
49
+ pattern: "boolean? — treat path as a glob pattern for bulk update",
50
+ bodyFilter: "string? — narrow pattern matches by body content",
51
+ },
52
+ requiresInit: true,
53
+ longRunning: true,
54
+ });
55
+
56
+ r.register("get", {
57
+ handler: async (params, ctx) => {
58
+ return await this.#dispatchGet(params, ctx);
59
+ },
60
+ description:
61
+ "Promote an entry (or matching pattern) to visible visibility.",
62
+ params: {
63
+ run: "string — run alias",
64
+ path: "string — entry path or glob pattern",
65
+ bodyFilter: "string? — narrow pattern matches by body content",
66
+ visibility: "string? — target visibility (default: visible)",
67
+ },
68
+ requiresInit: true,
69
+ });
70
+
71
+ r.register("rm", {
72
+ handler: async (params, ctx) => {
73
+ return await this.#dispatchRm(params, ctx);
74
+ },
75
+ description: "Remove an entry's view (or matching pattern).",
76
+ params: {
77
+ run: "string — run alias",
78
+ path: "string — entry path or glob pattern",
79
+ bodyFilter: "string? — narrow pattern matches by body content",
80
+ },
81
+ requiresInit: true,
82
+ });
83
+
84
+ r.register("cp", {
85
+ handler: async (params, ctx) => {
86
+ const runRow = await this.#resolveRun(params.run, ctx);
87
+ await ctx.projectAgent.entries.cp({
88
+ runId: runRow.id,
89
+ from: params.from,
90
+ to: params.to,
91
+ visibility: params.visibility,
92
+ writer: "client",
93
+ });
94
+ return { ok: true };
95
+ },
96
+ description: "Copy an entry to a new path.",
97
+ params: {
98
+ run: "string — run alias",
99
+ from: "string — source path",
100
+ to: "string — destination path",
101
+ visibility: "string? — target visibility (default: visible)",
102
+ },
103
+ requiresInit: true,
104
+ });
105
+
106
+ r.register("mv", {
107
+ handler: async (params, ctx) => {
108
+ const runRow = await this.#resolveRun(params.run, ctx);
109
+ await ctx.projectAgent.entries.mv({
110
+ runId: runRow.id,
111
+ from: params.from,
112
+ to: params.to,
113
+ visibility: params.visibility,
114
+ writer: "client",
115
+ });
116
+ return { ok: true };
117
+ },
118
+ description: "Rename an entry (copy then remove source).",
119
+ params: {
120
+ run: "string — run alias",
121
+ from: "string — source path",
122
+ to: "string — destination path",
123
+ visibility: "string? — target visibility (default: visible)",
124
+ },
125
+ requiresInit: true,
126
+ });
127
+
128
+ r.register("update", {
26
129
  handler: async (params, ctx) => {
130
+ const runRow = await this.#resolveRun(params.run, ctx);
131
+ const { status = 102, attributes = {} } = params;
132
+ const path = await ctx.projectAgent.entries.update({
133
+ runId: runRow.id,
134
+ body: params.body,
135
+ status,
136
+ attributes,
137
+ writer: "client",
138
+ });
139
+ return { ok: true, path };
140
+ },
141
+ description:
142
+ "Write an update:// entry carrying a turn's continuation/terminal " +
143
+ "signal. Not general — this is the lifecycle verb.",
144
+ params: {
145
+ run: "string — run alias",
146
+ body: "string — update text",
147
+ status:
148
+ "number? — 102 (continue) | 200/204 (terminal) | 422 (can't answer)",
149
+ attributes: "object? — extra attributes",
150
+ },
151
+ requiresInit: true,
152
+ });
153
+
154
+ // Connection handshake. First call a client makes. Establishes
155
+ // the project identity for this connection and announces the
156
+ // server's protocol version.
157
+ r.register("rummy/hello", {
158
+ handler: async (params, ctx) => {
159
+ const { RUMMY_PROTOCOL_VERSION } = await import(
160
+ "../../server/protocol.js"
161
+ );
162
+ if (params.clientVersion) {
163
+ const clientMajor = String(params.clientVersion).split(".")[0];
164
+ const serverMajor = RUMMY_PROTOCOL_VERSION.split(".")[0];
165
+ if (clientMajor !== serverMajor) {
166
+ throw new Error(
167
+ `protocol mismatch: server ${RUMMY_PROTOCOL_VERSION}, client ${params.clientVersion}. Clients must match MAJOR.`,
168
+ );
169
+ }
170
+ }
171
+ if (!params.name) throw new Error("rummy/hello: name is required");
172
+ if (!params.projectRoot) {
173
+ throw new Error("rummy/hello: projectRoot is required");
174
+ }
27
175
  const result = await ctx.projectAgent.init(
28
176
  params.name,
29
177
  params.projectRoot,
30
178
  params.configPath,
31
179
  );
32
180
  ctx.setContext(result.projectId, params.projectRoot);
33
- return result;
181
+ return {
182
+ rummyVersion: RUMMY_PROTOCOL_VERSION,
183
+ projectId: result.projectId,
184
+ projectRoot: params.projectRoot,
185
+ };
34
186
  },
35
- description: "Initialize project. Returns { projectId }.",
187
+ description:
188
+ "Connection handshake. First call a client makes. Establishes the " +
189
+ "project identity and returns the server's protocol version. " +
190
+ "Clients must match MAJOR or the call rejects.",
36
191
  params: {
37
192
  name: "string — project name (unique identifier)",
38
193
  projectRoot: "string — absolute path to source code",
39
194
  configPath: "string? — path to rummy config directory",
195
+ clientVersion:
196
+ "string? — client's protocol version; server rejects MAJOR mismatch",
40
197
  },
41
198
  });
42
199
 
@@ -44,10 +201,8 @@ export default class Rpc {
44
201
 
45
202
  r.register("getModels", {
46
203
  handler: async (params, ctx) => {
47
- const rows = await ctx.db.get_models.all({
48
- limit: params.limit ?? null,
49
- offset: params.offset ?? null,
50
- });
204
+ const { limit = null, offset = null } = params;
205
+ const rows = await ctx.db.get_models.all({ limit, offset });
51
206
  return rows.map((m) => ({
52
207
  alias: m.alias,
53
208
  actual: m.actual,
@@ -63,10 +218,11 @@ export default class Rpc {
63
218
 
64
219
  r.register("addModel", {
65
220
  handler: async (params, ctx) => {
221
+ const { contextLength = null } = params;
66
222
  const row = await ctx.db.upsert_model.get({
67
223
  alias: params.alias,
68
224
  actual: params.actual,
69
- context_length: params.contextLength ?? null,
225
+ context_length: contextLength,
70
226
  });
71
227
  return { id: row.id, alias: params.alias };
72
228
  },
@@ -87,316 +243,121 @@ export default class Rpc {
87
243
  params: { alias: "string — model alias to remove" },
88
244
  });
89
245
 
90
- // --- Entry operations (same dispatch as model) ---
91
-
92
- // Override: get has persist flag for file constraint management
93
- r.register("get", {
94
- handler: async (params, ctx) => {
95
- if (!params.path) throw new Error("path is required");
96
-
97
- if (params.persist) {
98
- const visibility = params.readonly ? "readonly" : "active";
99
- await File.setConstraint(
100
- ctx.db,
101
- ctx.projectId,
102
- params.path,
103
- visibility,
104
- );
105
- }
106
-
107
- if (!params.run) throw new Error("run is required");
108
- const { rummy } = await buildRunContext(hooks, ctx, params.run);
109
- await dispatchTool(hooks, rummy, "get", params.path, "", {
110
- path: params.path,
111
- });
112
- return { status: "ok" };
113
- },
114
- description: "Promote entry fidelity.",
115
- params: {
116
- path: "string — file path or glob pattern",
117
- run: "string — run alias",
118
- persist: "boolean? — also create file constraint",
119
- readonly: "boolean? — with persist, set readonly instead of active",
120
- },
121
- requiresInit: true,
122
- });
246
+ // --- File constraints (project-scoped overlay) ---
123
247
 
124
- // store is not a tool — it manages file constraints
125
- r.register("store", {
248
+ r.register("file/constraint", {
126
249
  handler: async (params, ctx) => {
127
- if (!params.path) throw new Error("path is required");
128
-
129
- if (params.clear) {
130
- await File.dropConstraint(ctx.db, ctx.projectId, params.path);
131
- return { status: "ok" };
250
+ if (!params.pattern) {
251
+ throw new Error("file/constraint: pattern is required");
132
252
  }
133
- if (params.persist) {
134
- const visibility = params.ignore ? "ignore" : "active";
135
- await File.setConstraint(
136
- ctx.db,
137
- ctx.projectId,
138
- params.path,
139
- visibility,
253
+ if (!CONSTRAINT_VISIBILITIES.has(params.visibility)) {
254
+ throw new Error(
255
+ `file/constraint: visibility must be one of ${[...CONSTRAINT_VISIBILITIES].join(", ")}`,
140
256
  );
141
257
  }
142
-
143
- if (!params.run) throw new Error("run is required");
144
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
145
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
146
- const store = ctx.projectAgent.entries;
147
- await store.demoteByPattern(runRow.id, params.path, null);
148
- return { status: "ok" };
149
- },
150
- description: "Demote entry to stored state.",
151
- params: {
152
- path: "string — file path or glob pattern",
153
- run: "string? — run alias (required without persist)",
154
- persist: "boolean? — also create file constraint",
155
- ignore: "boolean? — with persist, exclude from scan",
156
- clear: "boolean? — remove existing constraint",
157
- },
158
- requiresInit: true,
159
- });
160
-
161
- r.register("getEntries", {
162
- handler: async (params, ctx) => {
163
- let run;
164
- if (params.run) {
165
- run = await ctx.db.get_run_by_alias.get({ alias: params.run });
166
- } else {
167
- run = await ctx.db.get_latest_run.get({ project_id: ctx.projectId });
168
- }
169
- if (!run) return [];
170
- const entries = await ctx.projectAgent.entries.getEntriesByPattern(
171
- run.id,
172
- params.pattern ?? "*",
173
- params.body ?? null,
174
- { limit: params.limit ?? null, offset: params.offset ?? null },
175
- );
176
- return entries.map((e) => ({
177
- path: e.path,
178
- scheme: e.scheme,
179
- status: e.status,
180
- fidelity: e.fidelity,
181
- tokens: e.tokens,
182
- }));
183
- },
184
- description: "Query entries by pattern.",
185
- params: {
186
- pattern: "string? — glob pattern (default: *)",
187
- body: "string? — filter by body content",
188
- run: "string? — run alias (default: latest run)",
189
- limit: "number? — max results",
190
- offset: "number? — skip first N results",
191
- },
192
- requiresInit: true,
193
- });
194
-
195
- // --- Runs ---
196
-
197
- r.register("startRun", {
198
- handler: async (params, ctx) => {
199
- if (!params.model) throw new Error("model is required");
200
- const alias = `${params.model}_${Date.now()}`;
201
- const runRow = await ctx.db.create_run.get({
202
- project_id: ctx.projectId,
203
- parent_run_id: null,
204
- model: params.model ?? null,
205
- alias,
206
- temperature: params.temperature ?? null,
207
- persona: params.persona ?? null,
208
- context_limit: params.contextLimit ?? null,
209
- });
210
- return { run: alias, id: runRow.id };
211
- },
212
- description: "Pre-create a run. Returns { run, id }.",
213
- params: {
214
- model: "string — model alias (required)",
215
- temperature: "number? — 0 to 2",
216
- persona: "string?",
217
- contextLimit: "number?",
218
- },
219
- requiresInit: true,
220
- });
221
-
222
- r.register("ask", {
223
- handler: async (params, ctx) => {
224
- if (!params.model) throw new Error("model is required");
225
- return ctx.projectAgent.ask(
258
+ const normalized = await File.setConstraint(
259
+ ctx.db,
226
260
  ctx.projectId,
227
- params.model,
228
- params.prompt,
229
- params.run,
230
- {
231
- temperature: params.temperature ?? null,
232
- persona: params.persona ?? null,
233
- contextLimit: params.contextLimit,
234
- noRepo: params.noRepo,
235
- noInteraction: params.noInteraction,
236
- noProposals: params.noProposals,
237
- noWeb: params.noWeb,
238
- fork: params.fork,
239
- },
261
+ params.pattern,
262
+ params.visibility,
240
263
  );
264
+ return { ok: true, pattern: normalized };
241
265
  },
242
- description: "Non-mutating query. Model required.",
243
- longRunning: true,
266
+ description:
267
+ "Set a project-level file constraint. Visibility ∈ " +
268
+ "{active, readonly, ignore}. Patterns can be globs. " +
269
+ "Persists across runs; overlays git defaults.",
244
270
  params: {
245
- prompt: "string — user message",
246
- model: "string — model alias (required)",
247
- run: "string? — continue existing run",
248
- temperature: "number?",
249
- persona: "string?",
250
- contextLimit: "number?",
251
- noRepo: "boolean?",
252
- noInteraction: "boolean? — disable ask_user tool",
253
- noWeb: "boolean? — disable search and URL fetch",
254
- fork: "boolean?",
271
+ pattern: "string — file path or glob",
272
+ visibility: "string — active | readonly | ignore",
255
273
  },
256
274
  requiresInit: true,
257
275
  });
258
276
 
259
- r.register("act", {
277
+ r.register("file/drop", {
260
278
  handler: async (params, ctx) => {
261
- if (!params.model) throw new Error("model is required");
262
- return ctx.projectAgent.act(
279
+ if (!params.pattern) {
280
+ throw new Error("file/drop: pattern is required");
281
+ }
282
+ const normalized = await File.dropConstraint(
283
+ ctx.db,
263
284
  ctx.projectId,
264
- params.model,
265
- params.prompt,
266
- params.run,
267
- {
268
- temperature: params.temperature ?? null,
269
- persona: params.persona ?? null,
270
- contextLimit: params.contextLimit,
271
- noRepo: params.noRepo,
272
- noInteraction: params.noInteraction,
273
- noProposals: params.noProposals,
274
- noWeb: params.noWeb,
275
- fork: params.fork,
276
- },
285
+ params.pattern,
277
286
  );
287
+ return { ok: true, pattern: normalized };
278
288
  },
279
- description: "Mutating directive. Model required.",
280
- longRunning: true,
281
- params: {
282
- prompt: "string — user message",
283
- model: "string — model alias (required)",
284
- run: "string? — continue existing run",
285
- temperature: "number?",
286
- persona: "string?",
287
- contextLimit: "number?",
288
- noRepo: "boolean?",
289
- noInteraction: "boolean? — disable ask_user tool",
290
- noWeb: "boolean? — disable search and URL fetch",
291
- fork: "boolean?",
292
- },
289
+ description: "Remove a project-level file constraint.",
290
+ params: { pattern: "string — file path or glob to drop" },
293
291
  requiresInit: true,
294
292
  });
295
293
 
296
- r.register("run/resolve", {
297
- handler: async (params, ctx) =>
298
- ctx.projectAgent.resolve(params.run, params.resolution),
299
- description: "Resolve a proposed entry. Returns { run, status }.",
300
- longRunning: true,
301
- params: {
302
- run: "string — run alias",
303
- resolution: "{ path, action: 'accept'|'reject', output? }",
304
- },
305
- requiresInit: true,
306
- });
307
-
308
- r.register("run/abort", {
309
- handler: async (params, ctx) => {
310
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
311
- if (!runRow)
312
- throw new Error(msg("error.run_not_found", { runId: params.run }));
313
- ctx.projectAgent.abortRun(runRow.id);
314
- await ctx.db.update_run_status.run({
315
- id: runRow.id,
316
- status: 499,
294
+ r.register("getConstraints", {
295
+ handler: async (_params, ctx) => {
296
+ const rows = await ctx.db.get_file_constraints.all({
297
+ project_id: ctx.projectId,
317
298
  });
318
- return { status: "ok" };
319
- },
320
- description: "Abort run.",
321
- params: { run: "string — run alias" },
322
- requiresInit: true,
323
- });
324
-
325
- r.register("run/rename", {
326
- handler: async (params, ctx) => {
327
- const { run, name } = params;
328
- if (!name || !/^[a-zA-Z0-9_]+$/.test(name)) {
329
- throw new Error(msg("error.run_name_invalid"));
330
- }
331
- const runRow = await ctx.db.get_run_by_alias.get({ alias: run });
332
- if (!runRow)
333
- throw new Error(msg("error.run_not_found", { runId: run }));
334
- try {
335
- await ctx.db.rename_run.run({
336
- id: runRow.id,
337
- old_alias: runRow.alias,
338
- new_alias: name,
339
- });
340
- } catch (err) {
341
- if (err.message.includes("UNIQUE"))
342
- throw new Error(msg("error.run_name_taken", { name }));
343
- throw err;
344
- }
345
- return { run: name };
346
- },
347
- description: "Rename a run.",
348
- params: {
349
- run: "string — current run alias",
350
- name: "string — new name",
299
+ return rows.map((r) => ({
300
+ pattern: r.pattern,
301
+ visibility: r.visibility,
302
+ }));
351
303
  },
304
+ description:
305
+ "List project-level file constraints as [{pattern, visibility}].",
352
306
  requiresInit: true,
353
307
  });
354
308
 
355
- r.register("run/inject", {
356
- handler: async (params, ctx) =>
357
- ctx.projectAgent.inject(params.run, params.message),
358
- description: "Inject a message into a run.",
359
- longRunning: true,
360
- params: {
361
- run: "string — run alias",
362
- message: "string — message to inject",
363
- },
364
- requiresInit: true,
365
- });
309
+ // --- Queries ---
366
310
 
367
- r.register("run/config", {
311
+ r.register("getEntries", {
368
312
  handler: async (params, ctx) => {
369
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
370
- if (!runRow)
371
- throw new Error(msg("error.run_not_found", { runId: params.run }));
372
- await ctx.db.update_run_config.run({
373
- id: runRow.id,
374
- temperature: params.temperature ?? null,
375
- persona: params.persona ?? null,
376
- context_limit: params.contextLimit ?? null,
377
- model: params.model ?? null,
378
- });
379
- return { status: "ok" };
313
+ const runRow = await this.#resolveRun(params.run, ctx);
314
+ const { pattern = "*", bodyFilter = null } = params;
315
+ const rows = await ctx.projectAgent.entries.getEntriesByPattern(
316
+ runRow.id,
317
+ pattern,
318
+ bodyFilter,
319
+ );
320
+ return rows
321
+ .filter((e) => !params.scheme || e.scheme === params.scheme)
322
+ .filter((e) => !params.state || e.state === params.state)
323
+ .filter(
324
+ (e) => !params.visibility || e.visibility === params.visibility,
325
+ )
326
+ .map((e) => ({
327
+ path: e.path,
328
+ scheme: e.scheme,
329
+ state: e.state,
330
+ outcome: e.outcome,
331
+ visibility: e.visibility,
332
+ turn: e.turn,
333
+ tokens: e.tokens,
334
+ attributes:
335
+ typeof e.attributes === "string"
336
+ ? JSON.parse(e.attributes)
337
+ : e.attributes,
338
+ }));
380
339
  },
381
- description: "Update run configuration.",
340
+ description:
341
+ "List entries matching a pattern. Read-only — no promotion. " +
342
+ "Optional filters: scheme, state, visibility, bodyFilter.",
382
343
  params: {
383
344
  run: "string — run alias",
384
- temperature: "number?",
385
- persona: "string?",
386
- contextLimit: "number?",
387
- model: "string?",
345
+ pattern: "string? — glob pattern (default '*')",
346
+ scheme: "string? — filter by scheme (e.g. 'file')",
347
+ state: "string? — filter by state",
348
+ visibility: "string? — filter by visibility",
349
+ bodyFilter: "string? — narrow pattern matches by body content",
388
350
  },
389
351
  requiresInit: true,
390
352
  });
391
353
 
392
- // --- Queries ---
393
-
394
354
  r.register("getRuns", {
395
355
  handler: async (params, ctx) => {
356
+ const { limit = null, offset = null } = params;
396
357
  const rows = await ctx.db.get_runs_by_project.all({
397
358
  project_id: ctx.projectId,
398
- limit: params.limit ?? null,
399
- offset: params.offset ?? null,
359
+ limit,
360
+ offset,
400
361
  });
401
362
  return rows.map((row) => ({
402
363
  run: row.alias,
@@ -425,7 +386,7 @@ export default class Rpc {
425
386
  ctx.db.get_run_usage.get({ run_id: run.id }),
426
387
  ctx.db.get_reasoning.all({ run_id: run.id }),
427
388
  ctx.db.get_content.all({ run_id: run.id }),
428
- ctx.db.get_history.all({ run_id: run.id }),
389
+ ctx.db.get_results.all({ run_id: run.id }),
429
390
  ctx.db.get_latest_user_prompt.get({ run_id: run.id }),
430
391
  ctx.db.get_latest_summary.get({ run_id: run.id }),
431
392
  ]);
@@ -455,17 +416,14 @@ export default class Rpc {
455
416
  body: c.body,
456
417
  turn: c.turn,
457
418
  })),
458
- history: history.map((h) => {
459
- const scheme = h.path.split("://")[0];
460
- return {
461
- scheme,
462
- path: h.path,
463
- status: h.status,
464
- body: h.body,
465
- attributes: h.attributes ? JSON.parse(h.attributes) : null,
466
- turn: h.turn,
467
- };
468
- }),
419
+ history: history.map((h) => ({
420
+ tool: h.tool,
421
+ path: h.path,
422
+ status: h.status,
423
+ body: h.body,
424
+ attributes: h.attributes ? JSON.parse(h.attributes) : null,
425
+ turn: h.turn,
426
+ })),
469
427
  },
470
428
  last_user_prompt: promptRow?.body,
471
429
  last_summary: summaryRow?.body,
@@ -480,6 +438,11 @@ export default class Rpc {
480
438
 
481
439
  r.registerNotification("run/state", "Turn state update.");
482
440
  r.registerNotification("run/progress", "Turn status.");
441
+ r.registerNotification("run/proposal", "Proposal awaiting resolution.");
442
+ r.registerNotification(
443
+ "stream/cancelled",
444
+ "Server-initiated stream cancellation.",
445
+ );
483
446
  r.registerNotification("ui/render", "Streaming output.");
484
447
  r.registerNotification("ui/notify", "Toast notification.");
485
448
 
@@ -487,6 +450,257 @@ export default class Rpc {
487
450
  // Checked at request time — no timing dependency on plugin load order.
488
451
  r.setToolFallback(hooks, buildRunContext, dispatchTool);
489
452
  }
453
+
454
+ // --- Primitive dispatch helpers ---
455
+
456
+ async #resolveRun(runAlias, ctx) {
457
+ if (!runAlias) throw new Error("run is required");
458
+ const runRow = await ctx.db.get_run_by_alias.get({ alias: runAlias });
459
+ if (!runRow)
460
+ throw new Error(msg("error.run_not_found", { runId: runAlias }));
461
+ return runRow;
462
+ }
463
+
464
+ async #dispatchSet(params, ctx) {
465
+ if (!params.path) throw new Error("set: path is required");
466
+
467
+ // run:// is the lifecycle surface. A set to a brand-new run://
468
+ // alias starts a run loop; a state transition cancels or resolves.
469
+ if (params.path.startsWith("run://")) {
470
+ return await this.#dispatchRunSet(params, ctx);
471
+ }
472
+
473
+ const runRow = await this.#resolveRun(params.run, ctx);
474
+
475
+ // State transition on an existing proposed entry → route through
476
+ // AgentLoop.resolve, which applies scheme-specific side effects
477
+ // (patch application for set://, file removal for rm://, stream
478
+ // setup for sh:// / env://, etc.).
479
+ if (params.state && !params.append && !params.pattern) {
480
+ const current = await ctx.projectAgent.entries.getState(
481
+ runRow.id,
482
+ params.path,
483
+ );
484
+ if (current?.state === "proposed") {
485
+ const action =
486
+ params.state === "resolved"
487
+ ? "accept"
488
+ : params.state === "failed"
489
+ ? "error"
490
+ : params.state === "cancelled"
491
+ ? "reject"
492
+ : null;
493
+ if (action) {
494
+ const { body = null } = params;
495
+ return await ctx.projectAgent.resolve(params.run, {
496
+ path: params.path,
497
+ action,
498
+ output: body,
499
+ });
500
+ }
501
+ }
502
+ }
503
+
504
+ await ctx.projectAgent.entries.set({
505
+ runId: runRow.id,
506
+ projectId: ctx.projectId,
507
+ path: params.path,
508
+ body: params.body,
509
+ state: params.state,
510
+ visibility: params.visibility,
511
+ outcome: params.outcome,
512
+ attributes: params.attributes,
513
+ append: params.append,
514
+ pattern: params.pattern,
515
+ bodyFilter: params.bodyFilter,
516
+ writer: "client",
517
+ });
518
+ return { ok: true };
519
+ }
520
+
521
+ async #dispatchRunSet(params, ctx) {
522
+ let alias = params.path.slice("run://".length);
523
+
524
+ // Empty alias on a new-run set → synthesize ${model}_${epoch}.
525
+ // Matches AgentLoop.#generateAlias so server- and client-initiated
526
+ // runs share one naming scheme. Clients that want a specific name
527
+ // pass it in the path; anonymous starts get the synthesized one.
528
+ if (!alias) {
529
+ const { attributes: attrs = {} } = params;
530
+ if (!attrs.model) {
531
+ throw new Error(
532
+ "set run://: attributes.model is required when alias is omitted",
533
+ );
534
+ }
535
+ alias = `${attrs.model}_${Date.now()}`;
536
+ }
537
+
538
+ const existing = await ctx.db.get_run_by_alias.get({ alias });
539
+
540
+ const runPath = `run://${alias}`;
541
+
542
+ // State transition on an existing run.
543
+ if (existing && params.state) {
544
+ if (params.state === "cancelled") {
545
+ ctx.projectAgent.abortRun(existing.id);
546
+ }
547
+ await ctx.projectAgent.entries.set({
548
+ runId: existing.id,
549
+ path: runPath,
550
+ state: params.state,
551
+ outcome: params.outcome,
552
+ writer: "client",
553
+ });
554
+ return { ok: true, alias };
555
+ }
556
+
557
+ // New run — kick off the loop. AgentLoop handles row + entry creation.
558
+ if (!existing) {
559
+ const { attributes: attrs = {} } = params;
560
+ if (!attrs.model) {
561
+ throw new Error(
562
+ "set run://: attributes.model is required for a new run",
563
+ );
564
+ }
565
+ const { mode } = attrs;
566
+ if (mode !== "ask" && mode !== "act") {
567
+ throw new Error(
568
+ `set run://: attributes.mode is required and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
569
+ );
570
+ }
571
+ const options = {
572
+ temperature: attrs.temperature,
573
+ persona: attrs.persona,
574
+ contextLimit: attrs.contextLimit,
575
+ noRepo: attrs.noRepo,
576
+ noInteraction: attrs.noInteraction,
577
+ noWeb: attrs.noWeb,
578
+ noProposals: attrs.noProposals,
579
+ fork: attrs.fork,
580
+ };
581
+ const { body = "" } = params;
582
+ // Fire-and-forget: client watches state via entry notifications.
583
+ // ProjectAgent exposes .ask/.act wrappers over AgentLoop#run; route
584
+ // by mode rather than calling the private loop directly.
585
+ const kickoff =
586
+ mode === "act"
587
+ ? ctx.projectAgent.act(
588
+ ctx.projectId,
589
+ attrs.model,
590
+ body,
591
+ alias,
592
+ options,
593
+ )
594
+ : ctx.projectAgent.ask(
595
+ ctx.projectId,
596
+ attrs.model,
597
+ body,
598
+ alias,
599
+ options,
600
+ );
601
+ kickoff.catch((err) => {
602
+ console.error(`[RUMMY] run ${alias} crashed: ${err.message}`);
603
+ });
604
+ return { ok: true, alias };
605
+ }
606
+
607
+ // Existing run + fork=true: create a child run synchronously so we
608
+ // can return the child alias, then kick off the loop against it.
609
+ // fork needs a brand-new run row with parent_run_id set; inject()
610
+ // would just add another prompt to the parent.
611
+ const attrs = params.attributes ? params.attributes : {};
612
+ if (attrs.fork === true) {
613
+ const { mode } = attrs;
614
+ if (mode !== "ask" && mode !== "act") {
615
+ throw new Error(
616
+ `set run://: attributes.mode is required on fork and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
617
+ );
618
+ }
619
+ const model = attrs.model ? attrs.model : existing.model;
620
+ const prompt = params.body ? params.body : "";
621
+ const childInfo = await ctx.projectAgent.ensureRun(
622
+ ctx.projectId,
623
+ model,
624
+ alias,
625
+ prompt,
626
+ {
627
+ fork: true,
628
+ temperature: attrs.temperature,
629
+ persona: attrs.persona,
630
+ contextLimit: attrs.contextLimit,
631
+ },
632
+ );
633
+ const options = {
634
+ temperature: attrs.temperature,
635
+ persona: attrs.persona,
636
+ contextLimit: attrs.contextLimit,
637
+ noRepo: attrs.noRepo,
638
+ noInteraction: attrs.noInteraction,
639
+ noWeb: attrs.noWeb,
640
+ noProposals: attrs.noProposals,
641
+ // fork already applied — pass false to reuse the child row.
642
+ fork: false,
643
+ };
644
+ const kickoff =
645
+ mode === "act"
646
+ ? ctx.projectAgent.act(
647
+ ctx.projectId,
648
+ model,
649
+ prompt,
650
+ childInfo.alias,
651
+ options,
652
+ )
653
+ : ctx.projectAgent.ask(
654
+ ctx.projectId,
655
+ model,
656
+ prompt,
657
+ childInfo.alias,
658
+ options,
659
+ );
660
+ kickoff.catch((err) => {
661
+ console.error(
662
+ `[RUMMY] fork ${childInfo.alias} crashed: ${err.message}`,
663
+ );
664
+ });
665
+ return { ok: true, alias: childInfo.alias };
666
+ }
667
+
668
+ // Existing run with body-only update (continuation prompt). Inject.
669
+ if (params.body) {
670
+ const { mode } = attrs;
671
+ if (mode !== "ask" && mode !== "act") {
672
+ throw new Error(
673
+ `set run://: attributes.mode is required on inject and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
674
+ );
675
+ }
676
+ await ctx.projectAgent.inject(alias, params.body, mode);
677
+ return { ok: true, alias };
678
+ }
679
+
680
+ return { ok: true, alias };
681
+ }
682
+
683
+ async #dispatchGet(params, ctx) {
684
+ const runRow = await this.#resolveRun(params.run, ctx);
685
+ await ctx.projectAgent.entries.get({
686
+ runId: runRow.id,
687
+ turn: await ctx.projectAgent.entries.nextTurn(runRow.id),
688
+ path: params.path,
689
+ bodyFilter: params.bodyFilter,
690
+ visibility: params.visibility,
691
+ });
692
+ return { ok: true };
693
+ }
694
+
695
+ async #dispatchRm(params, ctx) {
696
+ const runRow = await this.#resolveRun(params.run, ctx);
697
+ await ctx.projectAgent.entries.rm({
698
+ runId: runRow.id,
699
+ path: params.path,
700
+ bodyFilter: params.bodyFilter,
701
+ });
702
+ return { ok: true };
703
+ }
490
704
  }
491
705
 
492
706
  async function buildRunContext(hooks, ctx, runAlias) {
@@ -517,14 +731,19 @@ async function buildRunContext(hooks, ctx, runAlias) {
517
731
 
518
732
  async function dispatchTool(hooks, rummy, scheme, path, body, attributes) {
519
733
  const store = rummy.entries;
520
- const resultPath = await store.dedup(
734
+ const resultPath = await store.logPath(
521
735
  rummy.runId,
736
+ rummy.sequence,
522
737
  scheme,
523
738
  path,
524
- rummy.sequence,
525
739
  );
526
740
 
527
- await store.upsert(rummy.runId, rummy.sequence, resultPath, body, 200, {
741
+ await store.set({
742
+ runId: rummy.runId,
743
+ turn: rummy.sequence,
744
+ path: resultPath,
745
+ body,
746
+ state: "resolved",
528
747
  attributes: attributes,
529
748
  loopId: rummy.loopId,
530
749
  });
@@ -534,7 +753,7 @@ async function dispatchTool(hooks, rummy, scheme, path, body, attributes) {
534
753
  path: resultPath,
535
754
  body: body,
536
755
  attributes: attributes,
537
- status: 200,
756
+ state: "resolved",
538
757
  resultPath,
539
758
  };
540
759