@possumtech/rummy 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -0
- package/SPEC.md +84 -0
- package/package.json +8 -8
- package/scriptify/ask_run.js +77 -0
- package/src/agent/AgentLoop.js +30 -19
- package/src/agent/Entries.js +23 -2
- package/src/agent/ProjectAgent.js +2 -2
- package/src/agent/TurnExecutor.js +3 -0
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +5 -0
- package/src/agent/materializeContext.js +4 -2
- package/src/agent/runs.sql +19 -0
- package/src/agent/tokens.js +6 -0
- package/src/hooks/RummyContext.js +4 -0
- package/src/llm/LlmProvider.js +24 -21
- package/src/llm/errors.js +1 -1
- package/src/llm/retry.js +63 -0
- package/src/plugins/budget/budget.js +64 -18
- package/src/plugins/get/getDoc.md +3 -3
- package/src/plugins/instructions/instructions.js +123 -1
- package/src/plugins/instructions/instructions.md +20 -12
- package/src/plugins/instructions/instructions_104.md +4 -4
- package/src/plugins/instructions/instructions_105.md +28 -36
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -8
- package/src/plugins/known/known.js +2 -1
- package/src/plugins/log/log.js +27 -7
- package/src/plugins/prompt/prompt.js +10 -4
- package/src/plugins/rpc/rpc.js +11 -1
- package/src/plugins/update/update.js +18 -2
- package/src/plugins/yolo/yolo.js +192 -0
package/src/plugins/rpc/rpc.js
CHANGED
|
@@ -576,6 +576,7 @@ export default class Rpc {
|
|
|
576
576
|
noInteraction: attrs.noInteraction,
|
|
577
577
|
noWeb: attrs.noWeb,
|
|
578
578
|
noProposals: attrs.noProposals,
|
|
579
|
+
yolo: attrs.yolo,
|
|
579
580
|
fork: attrs.fork,
|
|
580
581
|
};
|
|
581
582
|
const { body = "" } = params;
|
|
@@ -638,6 +639,7 @@ export default class Rpc {
|
|
|
638
639
|
noInteraction: attrs.noInteraction,
|
|
639
640
|
noWeb: attrs.noWeb,
|
|
640
641
|
noProposals: attrs.noProposals,
|
|
642
|
+
yolo: attrs.yolo,
|
|
641
643
|
// fork already applied — pass false to reuse the child row.
|
|
642
644
|
fork: false,
|
|
643
645
|
};
|
|
@@ -673,7 +675,15 @@ export default class Rpc {
|
|
|
673
675
|
`set run://: attributes.mode is required on inject and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
|
|
674
676
|
);
|
|
675
677
|
}
|
|
676
|
-
|
|
678
|
+
const options = {
|
|
679
|
+
temperature: attrs.temperature,
|
|
680
|
+
noRepo: attrs.noRepo,
|
|
681
|
+
noInteraction: attrs.noInteraction,
|
|
682
|
+
noWeb: attrs.noWeb,
|
|
683
|
+
noProposals: attrs.noProposals,
|
|
684
|
+
yolo: attrs.yolo,
|
|
685
|
+
};
|
|
686
|
+
await ctx.projectAgent.inject(alias, params.body, mode, options);
|
|
677
687
|
return { ok: true, alias };
|
|
678
688
|
}
|
|
679
689
|
|
|
@@ -33,7 +33,22 @@ export default class Update {
|
|
|
33
33
|
|
|
34
34
|
async handler(entry, rummy) {
|
|
35
35
|
const status = entry.attributes?.status ?? 102;
|
|
36
|
-
await rummy.
|
|
36
|
+
const validation = await rummy.hooks.instructions.validateNavigation(
|
|
37
|
+
status,
|
|
38
|
+
rummy,
|
|
39
|
+
);
|
|
40
|
+
const attributes = validation.ok ? {} : { rejected: true };
|
|
41
|
+
await rummy.update(entry.body, { status, attributes });
|
|
42
|
+
if (!validation.ok) {
|
|
43
|
+
await rummy.hooks.error.log.emit({
|
|
44
|
+
store: rummy.entries,
|
|
45
|
+
runId: rummy.runId,
|
|
46
|
+
turn: rummy.sequence,
|
|
47
|
+
loopId: rummy.loopId,
|
|
48
|
+
message: validation.reason,
|
|
49
|
+
status: 422,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
/**
|
|
@@ -50,7 +65,8 @@ export default class Update {
|
|
|
50
65
|
async resolve({ recorded, content, runId, turn, loopId, rummy }) {
|
|
51
66
|
const entry = recorded.findLast((e) => e.scheme === "update");
|
|
52
67
|
const status = entry?.attributes?.status ?? 102;
|
|
53
|
-
const
|
|
68
|
+
const rejected = entry?.attributes?.rejected === true;
|
|
69
|
+
const isTerminal = TERMINAL_STATUSES.has(status) && !rejected;
|
|
54
70
|
let summaryText = null;
|
|
55
71
|
let updateText = null;
|
|
56
72
|
if (entry?.body) {
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { logPathToDataBase } from "../helpers.js";
|
|
3
|
+
|
|
4
|
+
const SH_PATH_RE = /^log:\/\/turn_\d+\/(sh|env)\//;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* YOLO plugin — for runs started with `yolo: true`, auto-resolves every
|
|
8
|
+
* proposal server-side and spawns sh/env commands locally, streaming
|
|
9
|
+
* output to the same data-channel entries the existing `stream`/
|
|
10
|
+
* `stream/completed` RPC contract uses.
|
|
11
|
+
*
|
|
12
|
+
* Pattern parallel to `noRepo`/`noWeb`/`noInteraction`/`noProposals`:
|
|
13
|
+
* `yolo` is a run attribute plumbed via rpc.js → AgentLoop loop config →
|
|
14
|
+
* RummyContext.yolo. This plugin reads `rummy.yolo` off the proposal
|
|
15
|
+
* payload and engages only when set; non-yolo runs are unaffected.
|
|
16
|
+
*
|
|
17
|
+
* The plugin replicates AgentLoop.resolve()'s accept path inline rather
|
|
18
|
+
* than calling an exposed projectAgent — keeps yolo logic contained in
|
|
19
|
+
* the yolo plugin and out of backbone files.
|
|
20
|
+
*/
|
|
21
|
+
export default class Yolo {
|
|
22
|
+
constructor(core) {
|
|
23
|
+
this.core = core;
|
|
24
|
+
core.hooks.proposal.pending.on(this.#onPending.bind(this));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async #onPending({ run, proposed, rummy }) {
|
|
28
|
+
if (!rummy?.yolo) return;
|
|
29
|
+
for (const p of proposed) {
|
|
30
|
+
// Resolve first — that fires proposal.accepted, which lets the
|
|
31
|
+
// sh/env plugin seed the streaming channel entries. Then spawn
|
|
32
|
+
// into those existing channels. If we spawned first, sh.js's
|
|
33
|
+
// post-accept channel creation would clobber the body we just
|
|
34
|
+
// streamed (sets state=streaming, body="").
|
|
35
|
+
await this.#serverResolve(rummy, p.path);
|
|
36
|
+
if (SH_PATH_RE.test(p.path)) {
|
|
37
|
+
await this.#executeShellProposal(rummy, p.path);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Replicate AgentLoop.resolve()'s accept path: accepting filter
|
|
44
|
+
* (veto check), content filter (resolved body), set state="resolved",
|
|
45
|
+
* emit proposal.accepted for plugin side effects.
|
|
46
|
+
*/
|
|
47
|
+
async #serverResolve(rummy, path) {
|
|
48
|
+
const runId = rummy.runId;
|
|
49
|
+
const entries = rummy.entries;
|
|
50
|
+
const db = rummy.db;
|
|
51
|
+
const runRow = await db.get_run_by_id.get({ id: runId });
|
|
52
|
+
const project = await db.get_project_by_id.get({ id: runRow.project_id });
|
|
53
|
+
const attrs = await entries.getAttributes(runId, path);
|
|
54
|
+
const ctx = {
|
|
55
|
+
runId,
|
|
56
|
+
runRow,
|
|
57
|
+
projectId: runRow.project_id,
|
|
58
|
+
projectRoot: project?.project_root,
|
|
59
|
+
path,
|
|
60
|
+
attrs,
|
|
61
|
+
output: "",
|
|
62
|
+
db,
|
|
63
|
+
entries,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const veto = await this.core.hooks.proposal.accepting.filter(null, ctx);
|
|
67
|
+
if (veto?.allow === false) {
|
|
68
|
+
await entries.set({
|
|
69
|
+
runId,
|
|
70
|
+
path,
|
|
71
|
+
state: "failed",
|
|
72
|
+
outcome: veto.outcome,
|
|
73
|
+
body: veto.body,
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const resolvedBody = await this.core.hooks.proposal.content.filter("", ctx);
|
|
79
|
+
const existing = await entries.getState(runId, path);
|
|
80
|
+
const existingTurn = existing?.turn === undefined ? 0 : existing.turn;
|
|
81
|
+
await entries.set({
|
|
82
|
+
runId,
|
|
83
|
+
turn: existingTurn,
|
|
84
|
+
path,
|
|
85
|
+
state: "resolved",
|
|
86
|
+
body: resolvedBody,
|
|
87
|
+
});
|
|
88
|
+
await this.core.hooks.proposal.accepted.emit({ ...ctx, resolvedBody });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Spawn the sh/env command locally and stream stdout/stderr into
|
|
93
|
+
* `{dataBase}_1` and `{dataBase}_2` data entries. Mirrors the
|
|
94
|
+
* stream/stream-completed RPC contract — same channel layout, same
|
|
95
|
+
* terminal-state transitions on exit. Done inline (no RPC roundtrip)
|
|
96
|
+
* so the run is fully autonomous.
|
|
97
|
+
*/
|
|
98
|
+
async #executeShellProposal(rummy, logPath) {
|
|
99
|
+
const runId = rummy.runId;
|
|
100
|
+
const entries = rummy.entries;
|
|
101
|
+
const db = rummy.db;
|
|
102
|
+
const runRow = await db.get_run_by_id.get({ id: runId });
|
|
103
|
+
const project = await db.get_project_by_id.get({ id: runRow.project_id });
|
|
104
|
+
const projectRoot = project?.project_root;
|
|
105
|
+
if (!projectRoot) return;
|
|
106
|
+
|
|
107
|
+
const attrs = await entries.getAttributes(runId, logPath);
|
|
108
|
+
const command = attrs?.command || attrs?.summary;
|
|
109
|
+
if (!command) return;
|
|
110
|
+
|
|
111
|
+
const dataBase = logPathToDataBase(logPath);
|
|
112
|
+
if (!dataBase) return;
|
|
113
|
+
const stdoutPath = `${dataBase}_1`;
|
|
114
|
+
const stderrPath = `${dataBase}_2`;
|
|
115
|
+
|
|
116
|
+
const start = Date.now();
|
|
117
|
+
const child = spawn("bash", ["-lc", command], {
|
|
118
|
+
cwd: projectRoot,
|
|
119
|
+
env: process.env,
|
|
120
|
+
});
|
|
121
|
+
// Buffer chunks synchronously and write once after exit. Avoids
|
|
122
|
+
// the race where multiple async appends interleave with the
|
|
123
|
+
// terminal-state transition fired on 'close'.
|
|
124
|
+
const stdoutChunks = [];
|
|
125
|
+
const stderrChunks = [];
|
|
126
|
+
child.stdout.on("data", (data) => stdoutChunks.push(data.toString()));
|
|
127
|
+
child.stderr.on("data", (data) => stderrChunks.push(data.toString()));
|
|
128
|
+
|
|
129
|
+
await new Promise((resolve) => {
|
|
130
|
+
child.on("close", async (code) => {
|
|
131
|
+
const stdoutBody = stdoutChunks.join("");
|
|
132
|
+
const stderrBody = stderrChunks.join("");
|
|
133
|
+
if (stdoutBody) {
|
|
134
|
+
try {
|
|
135
|
+
await entries.set({
|
|
136
|
+
runId,
|
|
137
|
+
path: stdoutPath,
|
|
138
|
+
body: stdoutBody,
|
|
139
|
+
append: true,
|
|
140
|
+
});
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
if (stderrBody) {
|
|
144
|
+
try {
|
|
145
|
+
await entries.set({
|
|
146
|
+
runId,
|
|
147
|
+
path: stderrPath,
|
|
148
|
+
body: stderrBody,
|
|
149
|
+
append: true,
|
|
150
|
+
});
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
const exitCode = code === null ? 130 : code;
|
|
154
|
+
const duration = `${Math.round((Date.now() - start) / 1000)}s`;
|
|
155
|
+
const terminalState = exitCode === 0 ? "resolved" : "failed";
|
|
156
|
+
const outcome = exitCode === 0 ? null : `exit:${exitCode}`;
|
|
157
|
+
// Transition state without touching body — getState doesn't
|
|
158
|
+
// return body, and entries.set with body=undefined preserves
|
|
159
|
+
// the streamed content already in place. (`body: ""` would
|
|
160
|
+
// wipe everything we just streamed.)
|
|
161
|
+
for (const path of [stdoutPath, stderrPath]) {
|
|
162
|
+
try {
|
|
163
|
+
await entries.set({
|
|
164
|
+
runId,
|
|
165
|
+
path,
|
|
166
|
+
state: terminalState,
|
|
167
|
+
outcome,
|
|
168
|
+
});
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const channels = await entries.getEntriesByPattern(
|
|
173
|
+
runId,
|
|
174
|
+
`${dataBase}_*`,
|
|
175
|
+
null,
|
|
176
|
+
);
|
|
177
|
+
const summary = channels
|
|
178
|
+
.map((c) => `${c.path} (${c.tokens || 0} tokens)`)
|
|
179
|
+
.join(", ");
|
|
180
|
+
const exitLabel = exitCode === 0 ? "exit=0" : `exit=${exitCode}`;
|
|
181
|
+
await entries.set({
|
|
182
|
+
runId,
|
|
183
|
+
path: logPath,
|
|
184
|
+
state: "resolved",
|
|
185
|
+
body: `ran '${command}', ${exitLabel} (${duration}). Output: ${summary}`,
|
|
186
|
+
});
|
|
187
|
+
} catch {}
|
|
188
|
+
resolve();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|