@possumtech/rummy 2.0.1 → 2.1.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.
- package/.env.example +12 -7
- package/BENCH_ENVIRONMENT.md +230 -0
- package/CLIENT_INTERFACE.md +396 -0
- package/PLUGINS.md +93 -1
- package/SPEC.md +305 -28
- package/bin/postinstall.js +2 -2
- package/bin/rummy.js +2 -2
- package/last_run.txt +5617 -0
- package/migrations/001_initial_schema.sql +2 -1
- package/package.json +6 -2
- package/scriptify/cache_probe.js +66 -0
- package/scriptify/cache_probe_grok.js +74 -0
- package/service.js +22 -11
- package/src/agent/AgentLoop.js +33 -139
- package/src/agent/ContextAssembler.js +2 -9
- package/src/agent/Entries.js +36 -101
- package/src/agent/ProjectAgent.js +2 -9
- package/src/agent/TurnExecutor.js +45 -83
- package/src/agent/XmlParser.js +247 -273
- package/src/agent/budget.js +5 -28
- package/src/agent/config.js +38 -0
- package/src/agent/errors.js +7 -13
- package/src/agent/httpStatus.js +1 -19
- package/src/agent/known_store.sql +7 -2
- package/src/agent/materializeContext.js +12 -17
- package/src/agent/pathEncode.js +5 -0
- package/src/agent/rummyHome.js +9 -0
- package/src/agent/runs.sql +18 -0
- package/src/agent/tokens.js +2 -8
- package/src/hooks/HookRegistry.js +1 -16
- package/src/hooks/Hooks.js +8 -33
- package/src/hooks/PluginContext.js +3 -21
- package/src/hooks/RpcRegistry.js +1 -4
- package/src/hooks/RummyContext.js +2 -16
- package/src/hooks/ToolRegistry.js +5 -15
- package/src/llm/LlmProvider.js +28 -23
- package/src/llm/errors.js +41 -4
- package/src/llm/openaiStream.js +125 -0
- package/src/llm/retry.js +61 -15
- package/src/plugins/budget/budget.js +14 -81
- package/src/plugins/cli/README.md +87 -0
- package/src/plugins/cli/bin.js +61 -0
- package/src/plugins/cli/cli.js +120 -0
- package/src/plugins/env/README.md +2 -1
- package/src/plugins/env/env.js +4 -6
- package/src/plugins/env/envDoc.md +2 -2
- package/src/plugins/error/error.js +23 -23
- package/src/plugins/file/file.js +2 -22
- package/src/plugins/get/get.js +12 -34
- package/src/plugins/get/getDoc.md +5 -3
- package/src/plugins/hedberg/edits.js +1 -11
- package/src/plugins/hedberg/hedberg.js +3 -26
- package/src/plugins/hedberg/normalize.js +1 -5
- package/src/plugins/hedberg/patterns.js +4 -15
- package/src/plugins/hedberg/sed.js +1 -7
- package/src/plugins/helpers.js +28 -20
- package/src/plugins/index.js +25 -41
- package/src/plugins/instructions/README.md +18 -0
- package/src/plugins/instructions/instructions.js +13 -76
- package/src/plugins/instructions/instructions.md +19 -18
- package/src/plugins/instructions/instructions_104.md +5 -4
- package/src/plugins/instructions/instructions_105.md +16 -15
- package/src/plugins/instructions/instructions_106.md +15 -14
- package/src/plugins/instructions/instructions_107.md +13 -6
- package/src/plugins/known/README.md +26 -6
- package/src/plugins/known/known.js +36 -34
- package/src/plugins/log/README.md +2 -2
- package/src/plugins/log/log.js +6 -33
- package/src/plugins/ollama/ollama.js +50 -66
- package/src/plugins/openai/openai.js +26 -44
- package/src/plugins/openrouter/openrouter.js +28 -52
- package/src/plugins/policy/README.md +8 -2
- package/src/plugins/policy/policy.js +8 -21
- package/src/plugins/prompt/README.md +22 -0
- package/src/plugins/prompt/prompt.js +8 -16
- package/src/plugins/rm/rm.js +5 -2
- package/src/plugins/rm/rmDoc.md +4 -4
- package/src/plugins/rpc/README.md +2 -1
- package/src/plugins/rpc/rpc.js +51 -47
- package/src/plugins/set/README.md +5 -1
- package/src/plugins/set/set.js +23 -33
- package/src/plugins/set/setDoc.md +1 -1
- package/src/plugins/sh/README.md +2 -1
- package/src/plugins/sh/sh.js +5 -11
- package/src/plugins/sh/shDoc.md +2 -2
- package/src/plugins/stream/README.md +6 -5
- package/src/plugins/stream/stream.js +6 -35
- package/src/plugins/telemetry/telemetry.js +26 -19
- package/src/plugins/think/think.js +4 -7
- package/src/plugins/unknown/unknown.js +8 -13
- package/src/plugins/update/update.js +36 -35
- package/src/plugins/update/updateDoc.md +3 -3
- package/src/plugins/xai/xai.js +30 -20
- package/src/plugins/yolo/yolo.js +8 -41
- package/src/server/ClientConnection.js +17 -47
- package/src/server/SocketServer.js +14 -14
- package/src/server/protocol.js +1 -10
- package/src/sql/functions/slugify.js +5 -7
- package/src/sql/v_model_context.sql +4 -11
- package/turns/cli_1777462658211/turn_001.txt +772 -0
- package/turns/cli_1777462658211/turn_002.txt +606 -0
- package/turns/cli_1777462658211/turn_003.txt +667 -0
- package/turns/cli_1777462658211/turn_004.txt +297 -0
- package/turns/cli_1777462658211/turn_005.txt +301 -0
- package/turns/cli_1777462658211/turn_006.txt +262 -0
- package/turns/cli_1777465095132/turn_001.txt +715 -0
- package/turns/cli_1777465095132/turn_002.txt +236 -0
- package/turns/cli_1777465095132/turn_003.txt +287 -0
- package/turns/cli_1777465095132/turn_004.txt +694 -0
- package/turns/cli_1777465095132/turn_005.txt +422 -0
- package/turns/cli_1777465095132/turn_006.txt +365 -0
- package/turns/cli_1777465095132/turn_007.txt +885 -0
- package/turns/cli_1777465095132/turn_008.txt +1277 -0
- package/turns/cli_1777465095132/turn_009.txt +736 -0
|
@@ -14,3 +14,25 @@ Finds the latest `prompt://` entry in the turn_context rows. The mode
|
|
|
14
14
|
attribute (available tool list) and optional `warn` attribute in ask
|
|
15
15
|
mode. Falls back to the mode passed by the core if no prompt entry
|
|
16
16
|
exists.
|
|
17
|
+
|
|
18
|
+
## Archived prompts: the singular exception to invisibility
|
|
19
|
+
|
|
20
|
+
`v_model_context.sql` filters archived entries out of the model's
|
|
21
|
+
context — every scheme **except `prompt`**. Archived `prompt://`
|
|
22
|
+
entries flow through with `effective_visibility = 'archived'` and
|
|
23
|
+
their body suppressed (per `projected.body`'s visibility CASE). The
|
|
24
|
+
plugin then renders the tag with full attributes (`path`,
|
|
25
|
+
`visibility="archived"`, etc.) but empty body.
|
|
26
|
+
|
|
27
|
+
The exception exists because the prompt is run identity: every other
|
|
28
|
+
archived entry is recoverable by pattern search if the model ever
|
|
29
|
+
needs it back, but the prompt is the question the run is answering.
|
|
30
|
+
A model that loses sight of its prompt cannot honestly act. Keeping
|
|
31
|
+
the archived prompt's path visible lets the model emit
|
|
32
|
+
`<get path="prompt://N"/>` to promote it back if it archived
|
|
33
|
+
prematurely (or step back to an earlier stage via
|
|
34
|
+
`<update status="174">`).
|
|
35
|
+
|
|
36
|
+
This is the only entry-type exception to the "archived = invisible"
|
|
37
|
+
contract. New schemes that warrant similar treatment should be added
|
|
38
|
+
explicitly here, not by accident.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const SUMMARIZED_PROMPT_CHAR_CAP = 500;
|
|
2
|
+
|
|
1
3
|
export default class Prompt {
|
|
2
4
|
#core;
|
|
3
5
|
|
|
@@ -7,28 +9,23 @@ export default class Prompt {
|
|
|
7
9
|
core.hooks.tools.onView(
|
|
8
10
|
"prompt",
|
|
9
11
|
(entry) => {
|
|
10
|
-
const limit = 500;
|
|
11
12
|
const full = entry.body;
|
|
12
|
-
if (full.length <=
|
|
13
|
-
return `${full.slice(0,
|
|
13
|
+
if (full.length <= SUMMARIZED_PROMPT_CHAR_CAP) return full;
|
|
14
|
+
return `${full.slice(0, SUMMARIZED_PROMPT_CHAR_CAP)}\n[truncated — promote to see the complete prompt]`;
|
|
14
15
|
},
|
|
15
16
|
"summarized",
|
|
16
17
|
);
|
|
17
18
|
core.on("turn.started", this.onTurnStarted.bind(this));
|
|
18
|
-
core.filter("assembly.user", this.assemblePrompt.bind(this),
|
|
19
|
+
core.filter("assembly.user", this.assemblePrompt.bind(this), 225);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
async onTurnStarted({ rummy, mode, prompt, isContinuation }) {
|
|
22
23
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
23
24
|
|
|
24
25
|
if (!isContinuation && prompt) {
|
|
25
|
-
//
|
|
26
|
-
// cycle. Archive prior cycles' prompts and per-turn logs so they
|
|
27
|
-
// don't pollute Deployment-landing validation. Knowns, unknowns,
|
|
28
|
-
// and file entries persist (cross-cycle knowledge survives).
|
|
26
|
+
// New prompt = new cycle; archive prior cycle's prompts/logs (knowns/unknowns persist).
|
|
29
27
|
await store.archivePriorPromptArtifacts(runId, turn);
|
|
30
28
|
|
|
31
|
-
// prompt:// writable_by: ["plugin"] — explicit for clarity.
|
|
32
29
|
await store.set({
|
|
33
30
|
runId,
|
|
34
31
|
turn,
|
|
@@ -43,7 +40,7 @@ export default class Prompt {
|
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
async assemblePrompt(content, ctx) {
|
|
46
|
-
const { rows,
|
|
43
|
+
const { rows, toolSet } = ctx;
|
|
47
44
|
const promptEntry = rows.findLast(
|
|
48
45
|
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
49
46
|
);
|
|
@@ -63,12 +60,7 @@ export default class Prompt {
|
|
|
63
60
|
let warn = "";
|
|
64
61
|
if (mode === "ask") warn = ' warn="File editing disallowed."';
|
|
65
62
|
|
|
66
|
-
//
|
|
67
|
-
// `reverted="N"` attribute on <prompt>. Historical error
|
|
68
|
-
// entries sit in <log> but read as ambient noise; this signal
|
|
69
|
-
// is dynamic and always fresh — the model sees that its
|
|
70
|
-
// promotions last turn were reverted, in the same spot where
|
|
71
|
-
// it reads budget numbers.
|
|
63
|
+
// reverted="N" surfaces last turn's 413 demotion count next to budget numbers.
|
|
72
64
|
let reverted = "";
|
|
73
65
|
const priorTurn = ctx.turn - 1;
|
|
74
66
|
if (priorTurn >= 1) {
|
package/src/plugins/rm/rm.js
CHANGED
|
@@ -27,9 +27,12 @@ export default class Rm {
|
|
|
27
27
|
await ctx.entries.rm({ runId: ctx.runId, path: target });
|
|
28
28
|
if (ctx.projectRoot) {
|
|
29
29
|
const { unlink } = await import("node:fs/promises");
|
|
30
|
-
const { join } = await import("node:path");
|
|
30
|
+
const { isAbsolute, join } = await import("node:path");
|
|
31
|
+
const targetPath = isAbsolute(target)
|
|
32
|
+
? target
|
|
33
|
+
: join(ctx.projectRoot, target);
|
|
31
34
|
try {
|
|
32
|
-
await unlink(
|
|
35
|
+
await unlink(targetPath);
|
|
33
36
|
} catch (err) {
|
|
34
37
|
// File may already be absent — entry rm'd regardless.
|
|
35
38
|
if (err.code !== "ENOENT") throw err;
|
package/src/plugins/rm/rmDoc.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Example: <rm path="src/config.js"/>
|
|
4
4
|
<!-- File removal. Simplest form. -->
|
|
5
5
|
|
|
6
|
-
Example: <rm path="known://temp_*"
|
|
7
|
-
<!--
|
|
6
|
+
Example: <rm path="known://temp_*" manifest/>
|
|
7
|
+
<!-- Optional: Manifest before deleting. Safety pattern for bulk operations. -->
|
|
8
8
|
|
|
9
9
|
* Permanent. Prefer <set path="..." visibility="archived"/> to preserve for later retrieval
|
|
10
10
|
<!-- Nudges toward archive over rm. Path attr included so the model sees a complete invocation shape, not a fragment. -->
|
|
11
11
|
|
|
12
|
-
* `
|
|
13
|
-
<!-- Canonical
|
|
12
|
+
* `manifest` lists what paths would be affected without performing the operation.
|
|
13
|
+
<!-- Canonical manifest teaching lives here — rm is the most intuitive 'check before committing' case. Model generalizes to cp/mv/get by analogy. Advanced uses (e.g. archive rediscovery via <get manifest>) belong in persona/skill docs, not here. -->
|
|
@@ -29,4 +29,5 @@ all registered tools.
|
|
|
29
29
|
- `getRuns`, `getRun`
|
|
30
30
|
|
|
31
31
|
### Notifications
|
|
32
|
-
- `run/
|
|
32
|
+
- `run/changed` — pulse: an entry under this run changed; client reconciles via `getEntries(run, { since })`.
|
|
33
|
+
- `ui/render`, `ui/notify`, `stream/cancelled`
|
package/src/plugins/rpc/rpc.js
CHANGED
|
@@ -24,10 +24,7 @@ export default class Rpc {
|
|
|
24
24
|
description: "Returns { methods, notifications } catalog.",
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
//
|
|
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.
|
|
27
|
+
// Primitives (SPEC #primitives); writer fixed to "client".
|
|
31
28
|
|
|
32
29
|
r.register("set", {
|
|
33
30
|
handler: async (params, ctx) => {
|
|
@@ -139,8 +136,9 @@ export default class Rpc {
|
|
|
139
136
|
return { ok: true, path };
|
|
140
137
|
},
|
|
141
138
|
description:
|
|
142
|
-
"Write
|
|
143
|
-
"signal. Not general — this is the
|
|
139
|
+
"Write a status update at log://turn_N/update/<slug> carrying a " +
|
|
140
|
+
"turn's continuation/terminal signal. Not general — this is the " +
|
|
141
|
+
"lifecycle verb.",
|
|
144
142
|
params: {
|
|
145
143
|
run: "string — run alias",
|
|
146
144
|
body: "string — update text",
|
|
@@ -151,9 +149,7 @@ export default class Rpc {
|
|
|
151
149
|
requiresInit: true,
|
|
152
150
|
});
|
|
153
151
|
|
|
154
|
-
// Connection handshake
|
|
155
|
-
// the project identity for this connection and announces the
|
|
156
|
-
// server's protocol version.
|
|
152
|
+
// Connection handshake; project identity + protocol version.
|
|
157
153
|
r.register("rummy/hello", {
|
|
158
154
|
handler: async (params, ctx) => {
|
|
159
155
|
const { RUMMY_PROTOCOL_VERSION } = await import(
|
|
@@ -311,11 +307,18 @@ export default class Rpc {
|
|
|
311
307
|
r.register("getEntries", {
|
|
312
308
|
handler: async (params, ctx) => {
|
|
313
309
|
const runRow = await this.#resolveRun(params.run, ctx);
|
|
314
|
-
const {
|
|
310
|
+
const {
|
|
311
|
+
pattern = "*",
|
|
312
|
+
bodyFilter = null,
|
|
313
|
+
since = null,
|
|
314
|
+
limit = null,
|
|
315
|
+
withBody = false,
|
|
316
|
+
} = params;
|
|
315
317
|
const rows = await ctx.projectAgent.entries.getEntriesByPattern(
|
|
316
318
|
runRow.id,
|
|
317
319
|
pattern,
|
|
318
320
|
bodyFilter,
|
|
321
|
+
{ since, limit },
|
|
319
322
|
);
|
|
320
323
|
return rows
|
|
321
324
|
.filter((e) => !params.scheme || e.scheme === params.scheme)
|
|
@@ -323,30 +326,43 @@ export default class Rpc {
|
|
|
323
326
|
.filter(
|
|
324
327
|
(e) => !params.visibility || e.visibility === params.visibility,
|
|
325
328
|
)
|
|
326
|
-
.map((e) =>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
329
|
+
.map((e) => {
|
|
330
|
+
const row = {
|
|
331
|
+
id: e.id,
|
|
332
|
+
path: e.path,
|
|
333
|
+
scheme: e.scheme,
|
|
334
|
+
state: e.state,
|
|
335
|
+
outcome: e.outcome,
|
|
336
|
+
visibility: e.visibility,
|
|
337
|
+
turn: e.turn,
|
|
338
|
+
tokens: e.tokens,
|
|
339
|
+
attributes:
|
|
340
|
+
typeof e.attributes === "string"
|
|
341
|
+
? JSON.parse(e.attributes)
|
|
342
|
+
: e.attributes,
|
|
343
|
+
};
|
|
344
|
+
if (withBody) row.body = e.body;
|
|
345
|
+
return row;
|
|
346
|
+
});
|
|
339
347
|
},
|
|
340
348
|
description:
|
|
341
349
|
"List entries matching a pattern. Read-only — no promotion. " +
|
|
342
|
-
"Optional filters: scheme, state, visibility, bodyFilter."
|
|
350
|
+
"Optional filters: scheme, state, visibility, bodyFilter. " +
|
|
351
|
+
"Pass `withBody: true` to include `body` on each row (omitted by default to keep pulse-reconcile traffic lean). " +
|
|
352
|
+
"For incremental sync after a `run/changed` pulse, pass `since` (last seen entry id); " +
|
|
353
|
+
"use `limit` to chunk catch-up.",
|
|
343
354
|
params: {
|
|
344
355
|
run: "string — run alias",
|
|
345
356
|
pattern: "string? — glob pattern (default '*')",
|
|
346
357
|
scheme: "string? — filter by scheme (e.g. 'file')",
|
|
347
358
|
state: "string? — filter by state",
|
|
348
359
|
visibility: "string? — filter by visibility",
|
|
349
|
-
bodyFilter:
|
|
360
|
+
bodyFilter:
|
|
361
|
+
"string? — filter rows by content of body (substring/glob; NOT for body inclusion — see withBody)",
|
|
362
|
+
withBody:
|
|
363
|
+
"boolean? — include `body` field on each returned row (default false)",
|
|
364
|
+
since: "number? — only entries with id > since (insertion-ordered)",
|
|
365
|
+
limit: "number? — cap result count",
|
|
350
366
|
},
|
|
351
367
|
requiresInit: true,
|
|
352
368
|
});
|
|
@@ -436,9 +452,10 @@ export default class Rpc {
|
|
|
436
452
|
|
|
437
453
|
// --- Notifications ---
|
|
438
454
|
|
|
439
|
-
r.registerNotification(
|
|
440
|
-
|
|
441
|
-
|
|
455
|
+
r.registerNotification(
|
|
456
|
+
"run/changed",
|
|
457
|
+
"Pulse: an entry under this run changed. Query with `getEntries(run, { pattern, since })` to reconcile.",
|
|
458
|
+
);
|
|
442
459
|
r.registerNotification(
|
|
443
460
|
"stream/cancelled",
|
|
444
461
|
"Server-initiated stream cancellation.",
|
|
@@ -446,8 +463,7 @@ export default class Rpc {
|
|
|
446
463
|
r.registerNotification("ui/render", "Streaming output.");
|
|
447
464
|
r.registerNotification("ui/notify", "Toast notification.");
|
|
448
465
|
|
|
449
|
-
//
|
|
450
|
-
// Checked at request time — no timing dependency on plugin load order.
|
|
466
|
+
// Any registered tool is callable via RPC; resolved at request time.
|
|
451
467
|
r.setToolFallback(hooks, buildRunContext, dispatchTool);
|
|
452
468
|
}
|
|
453
469
|
|
|
@@ -464,18 +480,14 @@ export default class Rpc {
|
|
|
464
480
|
async #dispatchSet(params, ctx) {
|
|
465
481
|
if (!params.path) throw new Error("set: path is required");
|
|
466
482
|
|
|
467
|
-
// run://
|
|
468
|
-
// alias starts a run loop; a state transition cancels or resolves.
|
|
483
|
+
// run:// = lifecycle surface (start run, cancel, resolve).
|
|
469
484
|
if (params.path.startsWith("run://")) {
|
|
470
485
|
return await this.#dispatchRunSet(params, ctx);
|
|
471
486
|
}
|
|
472
487
|
|
|
473
488
|
const runRow = await this.#resolveRun(params.run, ctx);
|
|
474
489
|
|
|
475
|
-
// State
|
|
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.).
|
|
490
|
+
// State transitions on proposed entries → AgentLoop.resolve for scheme-specific effects.
|
|
479
491
|
if (params.state && !params.append && !params.pattern) {
|
|
480
492
|
const current = await ctx.projectAgent.entries.getState(
|
|
481
493
|
runRow.id,
|
|
@@ -521,10 +533,7 @@ export default class Rpc {
|
|
|
521
533
|
async #dispatchRunSet(params, ctx) {
|
|
522
534
|
let alias = params.path.slice("run://".length);
|
|
523
535
|
|
|
524
|
-
// Empty alias
|
|
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.
|
|
536
|
+
// Empty alias → ${model}_${epoch}; mirrors AgentLoop.#generateAlias.
|
|
528
537
|
if (!alias) {
|
|
529
538
|
const { attributes: attrs = {} } = params;
|
|
530
539
|
if (!attrs.model) {
|
|
@@ -580,9 +589,7 @@ export default class Rpc {
|
|
|
580
589
|
fork: attrs.fork,
|
|
581
590
|
};
|
|
582
591
|
const { body = "" } = params;
|
|
583
|
-
// Fire-and-forget
|
|
584
|
-
// ProjectAgent exposes .ask/.act wrappers over AgentLoop#run; route
|
|
585
|
-
// by mode rather than calling the private loop directly.
|
|
592
|
+
// Fire-and-forget; client watches state via entry notifications.
|
|
586
593
|
const kickoff =
|
|
587
594
|
mode === "act"
|
|
588
595
|
? ctx.projectAgent.act(
|
|
@@ -605,10 +612,7 @@ export default class Rpc {
|
|
|
605
612
|
return { ok: true, alias };
|
|
606
613
|
}
|
|
607
614
|
|
|
608
|
-
//
|
|
609
|
-
// can return the child alias, then kick off the loop against it.
|
|
610
|
-
// fork needs a brand-new run row with parent_run_id set; inject()
|
|
611
|
-
// would just add another prompt to the parent.
|
|
615
|
+
// fork=true → new child run with parent_run_id; inject() would only add a prompt to parent.
|
|
612
616
|
const attrs = params.attributes ? params.attributes : {};
|
|
613
617
|
if (attrs.fork === true) {
|
|
614
618
|
const { mode } = attrs;
|
|
@@ -15,7 +15,7 @@ SEARCH/REPLACE edits, and pattern updates.
|
|
|
15
15
|
- **Category**: `logging`
|
|
16
16
|
- **Handler**: Routes based on attributes:
|
|
17
17
|
- `blocks` or `search` — SEARCH/REPLACE edit via `processEdit`.
|
|
18
|
-
- `
|
|
18
|
+
- `manifest` — pattern manifest (lists matches without performing the set).
|
|
19
19
|
- Scheme path — direct upsert at status 200.
|
|
20
20
|
- File path — produces status 202 (proposed) with unified diff patch.
|
|
21
21
|
- Glob/filter — bulk update via `updateBodyByPattern`.
|
|
@@ -31,3 +31,7 @@ the merge conflict block when a SEARCH/REPLACE was performed.
|
|
|
31
31
|
- **Heuristic fallback**: On literal failure, fuzzy matching with warnings.
|
|
32
32
|
- **Patch generation**: `generatePatch` produces unified diff for client display.
|
|
33
33
|
- File writes are always status 202 (proposed); scheme writes resolve immediately.
|
|
34
|
+
- **`proposal.content` filter** — when the client accepts a proposed
|
|
35
|
+
set, this plugin overrides the resolved body to the body it
|
|
36
|
+
already staged on the audit entry (rather than whatever literal
|
|
37
|
+
body the client passed through `resolve`).
|
package/src/plugins/set/set.js
CHANGED
|
@@ -79,12 +79,7 @@ export default class Set {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
const turn = (await db.get_run_by_id.get({ id: runId })).next_turn;
|
|
82
|
-
// Preserve
|
|
83
|
-
// earlier in the run may have promoted it. Updating the
|
|
84
|
-
// body without specifying visibility falls through to
|
|
85
|
-
// the data-category default ("summarized") and wipes
|
|
86
|
-
// the promotion, making the model re-get the file next
|
|
87
|
-
// turn (then cycle-strike out).
|
|
82
|
+
// Preserve current visibility; default would wipe an earlier <get>'s promotion.
|
|
88
83
|
const existingState = await entries.getState(runId, attrs.path);
|
|
89
84
|
await entries.set({
|
|
90
85
|
runId,
|
|
@@ -94,9 +89,13 @@ export default class Set {
|
|
|
94
89
|
visibility: existingState?.visibility,
|
|
95
90
|
});
|
|
96
91
|
if (projectRoot) {
|
|
97
|
-
const { writeFile } = await import("node:fs/promises");
|
|
98
|
-
const { join } = await import("node:path");
|
|
99
|
-
|
|
92
|
+
const { writeFile, mkdir } = await import("node:fs/promises");
|
|
93
|
+
const { dirname, isAbsolute, join } = await import("node:path");
|
|
94
|
+
const targetPath = isAbsolute(attrs.path)
|
|
95
|
+
? attrs.path
|
|
96
|
+
: join(projectRoot, attrs.path);
|
|
97
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
98
|
+
await writeFile(targetPath, patched);
|
|
100
99
|
}
|
|
101
100
|
if (isNewFile && projectId) {
|
|
102
101
|
await File.setConstraint(db, projectId, attrs.path, "active");
|
|
@@ -112,24 +111,22 @@ export default class Set {
|
|
|
112
111
|
const rawSummary = typeof attrs.summary === "string" ? attrs.summary : null;
|
|
113
112
|
const summaryText = rawSummary ? rawSummary.slice(0, 80) : null;
|
|
114
113
|
|
|
115
|
-
//
|
|
116
|
-
// error instead of falling through to the write path. Without
|
|
117
|
-
// this guard, a typo like visibility="promoted" (pre-migration
|
|
118
|
-
// terminology) silently body-wiped the target — the fidelity
|
|
119
|
-
// regression that cost us multiple demo runs.
|
|
114
|
+
// Reject invalid visibility on body-less set; otherwise a typo silently wipes the body.
|
|
120
115
|
if (
|
|
121
116
|
!entry.body &&
|
|
122
117
|
attrs.path &&
|
|
123
118
|
attrs.visibility !== undefined &&
|
|
124
119
|
!visibilityAttr
|
|
125
120
|
) {
|
|
126
|
-
await
|
|
127
|
-
store,
|
|
121
|
+
await store.set({
|
|
128
122
|
runId,
|
|
129
123
|
turn,
|
|
130
124
|
loopId,
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
path: entry.resultPath,
|
|
126
|
+
body: `Invalid visibility "${attrs.visibility}" on <set path="${attrs.path}"/>. Use visibility="visible|summarized|archived".`,
|
|
127
|
+
state: "failed",
|
|
128
|
+
outcome: "validation",
|
|
129
|
+
attributes: { path: attrs.path },
|
|
133
130
|
});
|
|
134
131
|
return;
|
|
135
132
|
}
|
|
@@ -187,8 +184,8 @@ export default class Set {
|
|
|
187
184
|
// Edit: sed patterns or SEARCH/REPLACE blocks
|
|
188
185
|
if (attrs.blocks || attrs.search != null) {
|
|
189
186
|
await this.#processEdit(rummy, entry, attrs);
|
|
190
|
-
} else if (attrs.
|
|
191
|
-
//
|
|
187
|
+
} else if (attrs.manifest && attrs.path) {
|
|
188
|
+
// Manifest: list paths and token costs without performing the operation.
|
|
192
189
|
const matches = await store.getEntriesByPattern(
|
|
193
190
|
runId,
|
|
194
191
|
attrs.path,
|
|
@@ -202,7 +199,7 @@ export default class Set {
|
|
|
202
199
|
attrs.path,
|
|
203
200
|
attrs.body,
|
|
204
201
|
matches,
|
|
205
|
-
{
|
|
202
|
+
{ manifest: true, loopId },
|
|
206
203
|
);
|
|
207
204
|
return;
|
|
208
205
|
} else {
|
|
@@ -262,8 +259,7 @@ export default class Set {
|
|
|
262
259
|
{ loopId },
|
|
263
260
|
);
|
|
264
261
|
} else {
|
|
265
|
-
// Direct scheme write
|
|
266
|
-
// Same result shape as file writes — diff against existing.
|
|
262
|
+
// Direct scheme write; same diff-against-existing shape as file writes.
|
|
267
263
|
const existing = await store.getBody(runId, target);
|
|
268
264
|
const oldContent = existing === null ? "" : existing;
|
|
269
265
|
const newContent = entry.body;
|
|
@@ -280,8 +276,7 @@ export default class Set {
|
|
|
280
276
|
path: target,
|
|
281
277
|
body: newContent,
|
|
282
278
|
state: "resolved",
|
|
283
|
-
// Scheme writes default
|
|
284
|
-
// it's material unless they explicitly demote/archive.
|
|
279
|
+
// Scheme writes default visible; the model wrote it.
|
|
285
280
|
visibility: visibilityAttr ? visibilityAttr : "visible",
|
|
286
281
|
attributes: summaryText ? { summary: summaryText } : null,
|
|
287
282
|
loopId,
|
|
@@ -340,8 +335,7 @@ export default class Set {
|
|
|
340
335
|
|
|
341
336
|
summary(entry) {
|
|
342
337
|
if (!entry.body) return "";
|
|
343
|
-
// Preserve SEARCH/REPLACE
|
|
344
|
-
// drops the before/after the model needs to recognize its edit.
|
|
338
|
+
// Preserve SEARCH/REPLACE blocks intact; truncation strips before/after the model needs.
|
|
345
339
|
if (/<<<<<<< SEARCH[\s\S]*>>>>>>> REPLACE/.test(entry.body)) {
|
|
346
340
|
return entry.body;
|
|
347
341
|
}
|
|
@@ -370,10 +364,7 @@ export default class Set {
|
|
|
370
364
|
|
|
371
365
|
for (const match of matches) {
|
|
372
366
|
if (match.scheme === null) {
|
|
373
|
-
// Bare file
|
|
374
|
-
// match body so the log carries a concrete before/after
|
|
375
|
-
// merge. #materializeRevisions still runs at turn-end to
|
|
376
|
-
// consolidate the set:// proposal for client acceptance.
|
|
367
|
+
// Bare file: apply edit immediately so log carries before/after merge.
|
|
377
368
|
const canonicalPath = `set://${match.path}`;
|
|
378
369
|
const revision = Set.#buildRevision(attrs);
|
|
379
370
|
const existingAttrs = await rummy.getAttributes(canonicalPath);
|
|
@@ -533,8 +524,7 @@ export default class Set {
|
|
|
533
524
|
}
|
|
534
525
|
}
|
|
535
526
|
|
|
536
|
-
// `replace`
|
|
537
|
-
// "delete the match"; normalize to empty string at this boundary.
|
|
527
|
+
// Missing `replace` = delete the match; normalize to empty string.
|
|
538
528
|
static #resolveReplace(attrs) {
|
|
539
529
|
return attrs.replace === undefined ? "" : attrs.replace;
|
|
540
530
|
}
|
|
@@ -18,5 +18,5 @@ Example: <set path="src/config.js">s/port = 3000/port = 8080/g;s/We're almost do
|
|
|
18
18
|
Example: <set path="example.md">Full file content here</set>
|
|
19
19
|
<!-- Create: body contents are entire file. -->
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
YOU MUST NOT use <sh></sh> or <env></env> to list, create, read, or edit files — use <get></get> and <set></set>
|
|
22
22
|
<!-- Reinforces at the decision point — model reading setDoc for file ops sees the prohibition here, not just buried in shDoc/envDoc which it may not be reading. -->
|
package/src/plugins/sh/README.md
CHANGED
|
@@ -24,7 +24,8 @@ record, one data payload:
|
|
|
24
24
|
- **Data channels**: `sh://turn_N/{slug}_1` (stdout), `sh://turn_N/{slug}_2`
|
|
25
25
|
(stderr) — scheme=`sh`, category=`data`. Created at status=102 on
|
|
26
26
|
proposal acceptance, grow via the `stream` RPC, transition to 200/500
|
|
27
|
-
via `stream/completed`. Render inside
|
|
27
|
+
via `stream/completed`. Render inside `<visible>` as `<sh>` when
|
|
28
|
+
promoted; listed in `<summarized>` otherwise.
|
|
28
29
|
|
|
29
30
|
The `sh` scheme exists **only** for the data channels. The proposal/log
|
|
30
31
|
entry itself is in the unified `log://` namespace along with every
|
package/src/plugins/sh/sh.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { logPathToDataBase } from "../helpers.js";
|
|
1
|
+
import { logPathToDataBase, streamSummary } from "../helpers.js";
|
|
2
2
|
import docs from "./shDoc.js";
|
|
3
3
|
|
|
4
4
|
const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
|
|
@@ -8,11 +8,7 @@ export default class Sh {
|
|
|
8
8
|
|
|
9
9
|
constructor(core) {
|
|
10
10
|
this.#core = core;
|
|
11
|
-
//
|
|
12
|
-
// data the model reads, not an audit record. The log entry at
|
|
13
|
-
// log://turn_N/sh/{slug} (scheme=log, category=logging) is the
|
|
14
|
-
// audit record; it lives in a separate namespace by design.
|
|
15
|
-
// See SPEC §streaming_entries and the scheme/category invariant.
|
|
11
|
+
// data scheme = streamed stdout/stderr; audit lives in log://. SPEC #streaming_entries.
|
|
16
12
|
core.registerScheme({ category: "data" });
|
|
17
13
|
core.on("handler", this.handler.bind(this));
|
|
18
14
|
core.on("visible", this.full.bind(this));
|
|
@@ -53,9 +49,7 @@ export default class Sh {
|
|
|
53
49
|
|
|
54
50
|
async handler(entry, rummy) {
|
|
55
51
|
const { entries: store, sequence: turn, runId, loopId } = rummy;
|
|
56
|
-
//
|
|
57
|
-
// body fills in on accept (log message about the action). Data
|
|
58
|
-
// entries with stdout/stderr are created on accept in resolve().
|
|
52
|
+
// 202 with command summary, empty body; stdout/stderr entries created on accept.
|
|
59
53
|
await store.set({
|
|
60
54
|
runId,
|
|
61
55
|
turn,
|
|
@@ -71,7 +65,7 @@ export default class Sh {
|
|
|
71
65
|
return `# sh ${entry.attributes.command}\n${entry.body}`;
|
|
72
66
|
}
|
|
73
67
|
|
|
74
|
-
summary() {
|
|
75
|
-
return "";
|
|
68
|
+
summary(entry) {
|
|
69
|
+
return streamSummary("sh", entry);
|
|
76
70
|
}
|
|
77
71
|
}
|
package/src/plugins/sh/shDoc.md
CHANGED
|
@@ -6,8 +6,8 @@ Example: <sh>npm install express</sh>
|
|
|
6
6
|
Example: <sh>npm test</sh>
|
|
7
7
|
<!-- Test execution. Another common side-effect action. -->
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>
|
|
10
10
|
<!-- Forces file operations through the entry system. -->
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
YOU MUST use <env></env> for commands without side effects
|
|
13
13
|
<!-- Reinforces the env/sh split. Read = env, mutate = sh. -->
|
|
@@ -16,12 +16,13 @@ A streaming action lives in **two namespaces** by design:
|
|
|
16
16
|
`{action}://turn_N/{slug}_2`, ... — scheme=`{action}` (sh, env, ...),
|
|
17
17
|
category=`data`. Created at status=102 on proposal acceptance. Grow
|
|
18
18
|
via `stream`; terminal via `stream/completed` / `stream/aborted` /
|
|
19
|
-
`stream/cancel`. Render inside `<
|
|
19
|
+
`stream/cancel`. Render inside `<visible>` (or `<summarized>` if
|
|
20
|
+
demoted).
|
|
20
21
|
|
|
21
|
-
The stream RPC `path` param is always the **log-entry path** (
|
|
22
|
-
|
|
23
|
-
base path internally
|
|
24
|
-
[scheme_category_split](#scheme_category_split).
|
|
22
|
+
The stream RPC `path` param is always the **log-entry path** (the
|
|
23
|
+
`log://...` path the client discovers via `getEntries` after a
|
|
24
|
+
`run/changed` pulse). The server derives the data base path internally
|
|
25
|
+
via `logPathToDataBase`. See [scheme_category_split](#scheme_category_split).
|
|
25
26
|
|
|
26
27
|
## RPC Methods
|
|
27
28
|
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { logPathToDataBase } from "../helpers.js";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
* Stream plugin — generic streaming entry infrastructure.
|
|
5
|
-
*
|
|
6
|
-
* Receives chunks from the client (or any producer) and appends them to
|
|
7
|
-
* existing data entries. Producers (sh/env handlers) create the data
|
|
8
|
-
* entries at status=102 on proposal acceptance; this plugin handles the
|
|
9
|
-
* subsequent append + terminal-status transition via two RPC methods.
|
|
10
|
-
*
|
|
11
|
-
* RPC `path` param is the **log-entry path** (log://turn_N/{action}/{slug}
|
|
12
|
-
* — that's what the client sees on `run/proposal`). Channels live under
|
|
13
|
-
* the producer scheme ({action}://turn_N/{slug}_N) for a clean
|
|
14
|
-
* data-vs-logging namespace split; this plugin derives the data base from
|
|
15
|
-
* the log path on every RPC call.
|
|
16
|
-
*
|
|
17
|
-
* Not a model-facing tool. No scheme, no tooldoc, no dispatch handler.
|
|
18
|
-
* Pure RPC plumbing that any streaming-producer plugin can leverage.
|
|
19
|
-
*/
|
|
3
|
+
// RPC plumbing that appends/terminates streaming data entries; see plugin README.
|
|
20
4
|
export default class Stream {
|
|
21
5
|
#core;
|
|
22
6
|
|
|
@@ -25,9 +9,7 @@ export default class Stream {
|
|
|
25
9
|
const hooks = core.hooks;
|
|
26
10
|
const r = hooks.rpc.registry;
|
|
27
11
|
|
|
28
|
-
// stream: append
|
|
29
|
-
// Entry path is constructed as `${path}_${channel}` per the Unix FD
|
|
30
|
-
// convention (1=stdout, 2=stderr, higher=other producer channels).
|
|
12
|
+
// stream: append chunk; channel = Unix FD (1=stdout, 2=stderr).
|
|
31
13
|
r.register("stream", {
|
|
32
14
|
handler: async (params, ctx) => {
|
|
33
15
|
if (!params.run) throw new Error("run is required");
|
|
@@ -67,8 +49,7 @@ export default class Stream {
|
|
|
67
49
|
requiresInit: true,
|
|
68
50
|
});
|
|
69
51
|
|
|
70
|
-
// stream/completed:
|
|
71
|
-
// to their terminal status and finalize the log entry body.
|
|
52
|
+
// stream/completed: terminal status on all channels + finalize log body.
|
|
72
53
|
r.register("stream/completed", {
|
|
73
54
|
handler: async (params, ctx) => {
|
|
74
55
|
if (!params.run) throw new Error("run is required");
|
|
@@ -107,8 +88,7 @@ export default class Stream {
|
|
|
107
88
|
});
|
|
108
89
|
}
|
|
109
90
|
|
|
110
|
-
//
|
|
111
|
-
// one line summarizing exit code, duration, and channel sizes.
|
|
91
|
+
// One-line final stats for the log entry body.
|
|
112
92
|
const logEntry = await store.getAttributes(runId, params.path);
|
|
113
93
|
let command = "";
|
|
114
94
|
if (logEntry?.command) command = logEntry.command;
|
|
@@ -138,11 +118,7 @@ export default class Stream {
|
|
|
138
118
|
requiresInit: true,
|
|
139
119
|
});
|
|
140
120
|
|
|
141
|
-
// stream/aborted: client
|
|
142
|
-
// channels to status 499 (Client Closed Request — the de-facto HTTP
|
|
143
|
-
// status for client-terminated requests) and rewrites the log entry
|
|
144
|
-
// body to note the abort. Shape mirrors stream/completed for client
|
|
145
|
-
// symmetry: same run/path addressing, same channel sweep.
|
|
121
|
+
// stream/aborted: client cancellation; channels → 499; mirrors stream/completed.
|
|
146
122
|
r.register("stream/aborted", {
|
|
147
123
|
handler: async (params, ctx) => {
|
|
148
124
|
if (!params.run) throw new Error("run is required");
|
|
@@ -211,12 +187,7 @@ export default class Stream {
|
|
|
211
187
|
requiresInit: true,
|
|
212
188
|
});
|
|
213
189
|
|
|
214
|
-
// stream/cancel: server-initiated
|
|
215
|
-
// internal server code) can cancel a streaming producer — the server
|
|
216
|
-
// transitions channels to 499 immediately and pushes a
|
|
217
|
-
// stream/cancelled notification so connected clients can kill their
|
|
218
|
-
// local processes. Also serves as stale 102 cleanup: if the client
|
|
219
|
-
// died mid-stream, call stream/cancel to mark orphaned entries terminal.
|
|
190
|
+
// stream/cancel: server-initiated; pushes stream/cancelled notification; cleans stale 102s.
|
|
220
191
|
r.register("stream/cancel", {
|
|
221
192
|
handler: async (params, ctx) => {
|
|
222
193
|
if (!params.run) throw new Error("run is required");
|