@possumtech/rummy 0.5.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 +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +306 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- 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 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +129 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +64 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +525 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +83 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/plugins/yolo/yolo.js +192 -0
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
package/src/agent/AgentLoop.js
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { computeBudget } from "./budget.js";
|
|
2
2
|
import msg from "./messages.js";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
const HTTP_TO_RUN_STATE = {
|
|
5
|
+
100: "proposed",
|
|
6
|
+
102: "streaming",
|
|
7
|
+
200: "resolved",
|
|
8
|
+
202: "streaming",
|
|
9
|
+
499: "cancelled",
|
|
10
|
+
500: "failed",
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
export default class AgentLoop {
|
|
6
14
|
#db;
|
|
7
15
|
#llmProvider;
|
|
8
16
|
#hooks;
|
|
9
17
|
#turnExecutor;
|
|
10
|
-
#
|
|
18
|
+
#entries;
|
|
11
19
|
#activeRuns = new Map();
|
|
12
20
|
|
|
13
|
-
constructor(db, llmProvider, hooks, turnExecutor,
|
|
21
|
+
constructor(db, llmProvider, hooks, turnExecutor, entries) {
|
|
14
22
|
this.#db = db;
|
|
15
23
|
this.#llmProvider = llmProvider;
|
|
16
24
|
this.#hooks = hooks;
|
|
17
25
|
this.#turnExecutor = turnExecutor;
|
|
18
|
-
this.#
|
|
26
|
+
this.#entries = entries;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
abort(runId) {
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
30
|
+
const active = this.#activeRuns.get(runId);
|
|
31
|
+
if (active) active.controller.abort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Abort every in-flight run and wait for each drain to settle.
|
|
36
|
+
* Called from server close / client teardown so the process can
|
|
37
|
+
* exit cleanly instead of leaving detached kickoff Promises
|
|
38
|
+
* pinning the event loop.
|
|
39
|
+
*/
|
|
40
|
+
async abortAll() {
|
|
41
|
+
const promises = [];
|
|
42
|
+
for (const { controller, promise } of this.#activeRuns.values()) {
|
|
43
|
+
controller.abort();
|
|
44
|
+
promises.push(promise);
|
|
45
|
+
}
|
|
46
|
+
// allSettled: drain waits for every run to finish; rejections are
|
|
47
|
+
// already surfaced to whoever awaited the original run() call.
|
|
48
|
+
await Promise.allSettled(promises);
|
|
24
49
|
}
|
|
25
50
|
|
|
26
51
|
async #generateAlias(modelAlias) {
|
|
@@ -31,9 +56,126 @@ export default class AgentLoop {
|
|
|
31
56
|
return `Turn ${turn}/${maxTurns}`;
|
|
32
57
|
}
|
|
33
58
|
|
|
34
|
-
async #
|
|
35
|
-
|
|
36
|
-
const
|
|
59
|
+
async #setRunStatus(runId, alias, httpStatus) {
|
|
60
|
+
await this.#db.update_run_status.run({ id: runId, status: httpStatus });
|
|
61
|
+
const state = HTTP_TO_RUN_STATE[httpStatus];
|
|
62
|
+
if (!state) return;
|
|
63
|
+
await this.#entries.set({
|
|
64
|
+
runId,
|
|
65
|
+
path: `run://${alias}`,
|
|
66
|
+
state,
|
|
67
|
+
writer: "system",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async #emitRunState({
|
|
72
|
+
projectId,
|
|
73
|
+
runId,
|
|
74
|
+
alias,
|
|
75
|
+
turn,
|
|
76
|
+
status,
|
|
77
|
+
contextSize,
|
|
78
|
+
result = null,
|
|
79
|
+
}) {
|
|
80
|
+
if (!contextSize) throw new Error("#emitRunState: contextSize is required");
|
|
81
|
+
const runUsage = await this.#db.get_run_usage.get({ run_id: runId });
|
|
82
|
+
const history = await this.#entries.getLog(runId);
|
|
83
|
+
const unknowns = await this.#entries.getUnknowns(runId);
|
|
84
|
+
const latestSummary = this.#hooks.instructions.findLatestSummary(history);
|
|
85
|
+
|
|
86
|
+
// Always emit complete telemetry. When we don't have a fresh turn
|
|
87
|
+
// result (abort/max-turns/crash), read the last turn's context
|
|
88
|
+
// tokens from the DB instead. Both code paths compute a real
|
|
89
|
+
// budget from real data — never undefined, never invented.
|
|
90
|
+
const rows = await this.#db.get_turn_context.all({
|
|
91
|
+
run_id: runId,
|
|
92
|
+
turn,
|
|
93
|
+
});
|
|
94
|
+
let totalTokens;
|
|
95
|
+
if (result) {
|
|
96
|
+
totalTokens = result.assembledTokens;
|
|
97
|
+
} else {
|
|
98
|
+
// No fresh turn result — this happens on abort/max-turns/crash
|
|
99
|
+
// emits that fire before any turn executed, or after a turn
|
|
100
|
+
// that never produced tokens. Read the last turn's assembled
|
|
101
|
+
// context_tokens from the DB; absent means no turn ran yet
|
|
102
|
+
// (zero is the truth, not a fallback).
|
|
103
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
104
|
+
run_id: runId,
|
|
105
|
+
});
|
|
106
|
+
totalTokens = lastCtx ? lastCtx.context_tokens : 0;
|
|
107
|
+
}
|
|
108
|
+
const budget = computeBudget({ rows, contextSize, totalTokens });
|
|
109
|
+
|
|
110
|
+
await this.#hooks.run.state.emit({
|
|
111
|
+
projectId,
|
|
112
|
+
run: alias,
|
|
113
|
+
turn,
|
|
114
|
+
status,
|
|
115
|
+
summary: latestSummary?.body,
|
|
116
|
+
history,
|
|
117
|
+
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
118
|
+
telemetry: {
|
|
119
|
+
modelAlias: result?.modelAlias,
|
|
120
|
+
model: result?.model,
|
|
121
|
+
temperature: result?.temperature,
|
|
122
|
+
context_size: contextSize,
|
|
123
|
+
context_tokens: totalTokens,
|
|
124
|
+
ceiling: budget.ceiling,
|
|
125
|
+
token_usage: budget.tokenUsage,
|
|
126
|
+
tokens_free: budget.tokensFree,
|
|
127
|
+
prompt_tokens: runUsage.prompt_tokens,
|
|
128
|
+
cached_tokens: runUsage.cached_tokens,
|
|
129
|
+
completion_tokens: runUsage.completion_tokens,
|
|
130
|
+
reasoning_tokens: runUsage.reasoning_tokens,
|
|
131
|
+
total_tokens: runUsage.total_tokens,
|
|
132
|
+
cost: runUsage.cost,
|
|
133
|
+
context_distribution: await this.#db.get_turn_distribution.all({
|
|
134
|
+
run_id: runId,
|
|
135
|
+
turn,
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async #writeRunEntry(
|
|
142
|
+
runId,
|
|
143
|
+
alias,
|
|
144
|
+
prompt,
|
|
145
|
+
{
|
|
146
|
+
projectId,
|
|
147
|
+
parentRunId,
|
|
148
|
+
model,
|
|
149
|
+
persona = null,
|
|
150
|
+
temperature = null,
|
|
151
|
+
contextLimit = null,
|
|
152
|
+
},
|
|
153
|
+
) {
|
|
154
|
+
await this.#entries.set({
|
|
155
|
+
runId,
|
|
156
|
+
turn: 0,
|
|
157
|
+
path: `run://${alias}`,
|
|
158
|
+
body: prompt ? prompt : "",
|
|
159
|
+
state: "proposed",
|
|
160
|
+
attributes: {
|
|
161
|
+
projectId,
|
|
162
|
+
parentRunId,
|
|
163
|
+
model,
|
|
164
|
+
persona,
|
|
165
|
+
temperature,
|
|
166
|
+
contextLimit,
|
|
167
|
+
},
|
|
168
|
+
writer: "system",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async ensureRun(projectId, model, run, prompt, options = {}) {
|
|
173
|
+
const {
|
|
174
|
+
fork: isFork = false,
|
|
175
|
+
temperature = null,
|
|
176
|
+
persona = null,
|
|
177
|
+
contextLimit = null,
|
|
178
|
+
} = options;
|
|
37
179
|
const requestedModel = model;
|
|
38
180
|
|
|
39
181
|
if (run && isFork) {
|
|
@@ -46,13 +188,18 @@ export default class AgentLoop {
|
|
|
46
188
|
parent_run_id: existingRun.id,
|
|
47
189
|
model: requestedModel,
|
|
48
190
|
alias,
|
|
49
|
-
temperature
|
|
50
|
-
persona
|
|
51
|
-
context_limit:
|
|
191
|
+
temperature,
|
|
192
|
+
persona,
|
|
193
|
+
context_limit: contextLimit,
|
|
52
194
|
});
|
|
53
|
-
await this.#
|
|
54
|
-
|
|
55
|
-
|
|
195
|
+
await this.#entries.forkEntries(existingRun.id, runRow.id);
|
|
196
|
+
await this.#writeRunEntry(runRow.id, alias, prompt, {
|
|
197
|
+
projectId,
|
|
198
|
+
parentRunId: existingRun.id,
|
|
199
|
+
model: requestedModel,
|
|
200
|
+
persona,
|
|
201
|
+
temperature,
|
|
202
|
+
contextLimit,
|
|
56
203
|
});
|
|
57
204
|
await this.#hooks.run.created.emit({
|
|
58
205
|
runId: runRow.id,
|
|
@@ -64,34 +211,43 @@ export default class AgentLoop {
|
|
|
64
211
|
|
|
65
212
|
if (run) {
|
|
66
213
|
const existingRun = await this.#db.get_run_by_alias.get({ alias: run });
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
214
|
+
if (existingRun) {
|
|
215
|
+
const existing = this.#activeRuns.get(existingRun.id);
|
|
216
|
+
if (existing) existing.controller.abort();
|
|
217
|
+
|
|
218
|
+
// Clean up stale proposals from interrupted runs
|
|
219
|
+
const unresolved = await this.#entries.getUnresolved(existingRun.id);
|
|
220
|
+
for (const u of unresolved) {
|
|
221
|
+
await this.#entries.set({
|
|
222
|
+
runId: existingRun.id,
|
|
223
|
+
path: u.path,
|
|
224
|
+
state: "cancelled",
|
|
225
|
+
body: "Stale proposal from interrupted run",
|
|
226
|
+
outcome: "interrupted",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return { runId: existingRun.id, alias: existingRun.alias };
|
|
82
230
|
}
|
|
83
|
-
|
|
231
|
+
// Client-specified alias for a brand-new run — accept it verbatim.
|
|
84
232
|
}
|
|
85
233
|
|
|
86
|
-
const alias = await this.#generateAlias(requestedModel);
|
|
234
|
+
const alias = run ? run : await this.#generateAlias(requestedModel);
|
|
87
235
|
const runRow = await this.#db.create_run.get({
|
|
88
236
|
project_id: projectId,
|
|
89
237
|
parent_run_id: null,
|
|
90
238
|
model: requestedModel,
|
|
91
239
|
alias,
|
|
92
|
-
temperature
|
|
93
|
-
persona
|
|
94
|
-
context_limit:
|
|
240
|
+
temperature,
|
|
241
|
+
persona,
|
|
242
|
+
context_limit: contextLimit,
|
|
243
|
+
});
|
|
244
|
+
await this.#writeRunEntry(runRow.id, alias, prompt, {
|
|
245
|
+
projectId,
|
|
246
|
+
parentRunId: null,
|
|
247
|
+
model: requestedModel,
|
|
248
|
+
persona,
|
|
249
|
+
temperature,
|
|
250
|
+
contextLimit,
|
|
95
251
|
});
|
|
96
252
|
await this.#hooks.run.created.emit({ runId: runRow.id, alias });
|
|
97
253
|
return { runId: runRow.id, alias };
|
|
@@ -119,13 +275,22 @@ export default class AgentLoop {
|
|
|
119
275
|
if (!project)
|
|
120
276
|
throw new Error(msg("error.project_not_found", { projectId }));
|
|
121
277
|
|
|
122
|
-
const noRepo = options?.noRepo ===
|
|
123
|
-
const noInteraction =
|
|
124
|
-
|
|
125
|
-
const
|
|
278
|
+
const noRepo = options?.noRepo ?? process.env.RUMMY_NO_REPO === "1";
|
|
279
|
+
const noInteraction =
|
|
280
|
+
options?.noInteraction ?? process.env.RUMMY_NO_INTERACTION === "1";
|
|
281
|
+
const noWeb = options?.noWeb ?? process.env.RUMMY_NO_WEB === "1";
|
|
282
|
+
const noProposals =
|
|
283
|
+
options?.noProposals ?? process.env.RUMMY_NO_PROPOSALS === "1";
|
|
284
|
+
const yolo = options?.yolo ?? process.env.RUMMY_YOLO === "1";
|
|
126
285
|
const requestedModel = model;
|
|
127
286
|
|
|
128
|
-
const runInfo = await this
|
|
287
|
+
const runInfo = await this.ensureRun(
|
|
288
|
+
projectId,
|
|
289
|
+
model,
|
|
290
|
+
run,
|
|
291
|
+
prompt,
|
|
292
|
+
options,
|
|
293
|
+
);
|
|
129
294
|
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
130
295
|
|
|
131
296
|
const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
@@ -134,12 +299,13 @@ export default class AgentLoop {
|
|
|
134
299
|
sequence: loopSeq.sequence,
|
|
135
300
|
mode,
|
|
136
301
|
model: requestedModel,
|
|
137
|
-
prompt: prompt
|
|
302
|
+
prompt: prompt ? prompt : "",
|
|
138
303
|
config: JSON.stringify({
|
|
139
304
|
noRepo,
|
|
140
305
|
noInteraction,
|
|
141
306
|
noWeb,
|
|
142
307
|
noProposals,
|
|
308
|
+
yolo,
|
|
143
309
|
temperature: options?.temperature,
|
|
144
310
|
}),
|
|
145
311
|
});
|
|
@@ -148,28 +314,53 @@ export default class AgentLoop {
|
|
|
148
314
|
return { run: currentAlias, status: 100 };
|
|
149
315
|
}
|
|
150
316
|
|
|
151
|
-
|
|
317
|
+
// Allocate the controller + Promise pair here so `abortAll` can
|
|
318
|
+
// reach both — abort the controller, await the Promise's drain.
|
|
319
|
+
const controller = new AbortController();
|
|
320
|
+
const promise = this.#drainQueue(
|
|
152
321
|
currentRunId,
|
|
153
322
|
currentAlias,
|
|
154
323
|
projectId,
|
|
155
324
|
project,
|
|
156
325
|
options,
|
|
326
|
+
controller,
|
|
157
327
|
);
|
|
328
|
+
this.#activeRuns.set(currentRunId, { controller, promise });
|
|
329
|
+
return promise;
|
|
158
330
|
}
|
|
159
331
|
|
|
160
|
-
async #drainQueue(
|
|
161
|
-
|
|
162
|
-
|
|
332
|
+
async #drainQueue(
|
|
333
|
+
currentRunId,
|
|
334
|
+
currentAlias,
|
|
335
|
+
projectId,
|
|
336
|
+
project,
|
|
337
|
+
options,
|
|
338
|
+
controller,
|
|
339
|
+
) {
|
|
340
|
+
console.error(`[DRAIN] ${currentAlias} enter (runId=${currentRunId})`);
|
|
163
341
|
|
|
164
342
|
try {
|
|
165
343
|
while (true) {
|
|
166
344
|
const loop = await this.#db.claim_next_loop.get({
|
|
167
345
|
run_id: currentRunId,
|
|
168
346
|
});
|
|
169
|
-
if (!loop)
|
|
347
|
+
if (!loop) {
|
|
348
|
+
console.error(`[DRAIN] ${currentAlias} queue empty — exiting`);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
console.error(
|
|
352
|
+
`[DRAIN] ${currentAlias} claimed loop id=${loop.id} mode=${loop.mode} seq=${loop.sequence}`,
|
|
353
|
+
);
|
|
170
354
|
|
|
171
|
-
const loopConfig =
|
|
355
|
+
const loopConfig = JSON.parse(loop.config);
|
|
172
356
|
const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
357
|
+
const {
|
|
358
|
+
noRepo = false,
|
|
359
|
+
noInteraction = false,
|
|
360
|
+
noWeb = false,
|
|
361
|
+
noProposals = false,
|
|
362
|
+
yolo = false,
|
|
363
|
+
} = loopConfig;
|
|
173
364
|
|
|
174
365
|
let result;
|
|
175
366
|
try {
|
|
@@ -182,15 +373,19 @@ export default class AgentLoop {
|
|
|
182
373
|
currentLoopId: loop.id,
|
|
183
374
|
requestedModel: loop.model,
|
|
184
375
|
prompt: loop.prompt,
|
|
185
|
-
noRepo
|
|
186
|
-
noInteraction
|
|
187
|
-
noWeb
|
|
188
|
-
noProposals
|
|
376
|
+
noRepo,
|
|
377
|
+
noInteraction,
|
|
378
|
+
noWeb,
|
|
379
|
+
noProposals,
|
|
380
|
+
yolo,
|
|
189
381
|
options: { ...options, temperature: loopConfig.temperature },
|
|
190
382
|
hook,
|
|
191
383
|
signal: controller.signal,
|
|
192
384
|
});
|
|
193
385
|
} catch (err) {
|
|
386
|
+
console.error(
|
|
387
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} threw: ${err.message}`,
|
|
388
|
+
);
|
|
194
389
|
await this.#db.complete_loop.run({
|
|
195
390
|
id: loop.id,
|
|
196
391
|
status: 500,
|
|
@@ -200,6 +395,9 @@ export default class AgentLoop {
|
|
|
200
395
|
}
|
|
201
396
|
|
|
202
397
|
if (result.status === 413) {
|
|
398
|
+
console.error(
|
|
399
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} overflow=413`,
|
|
400
|
+
);
|
|
203
401
|
await this.#db.complete_loop.run({
|
|
204
402
|
id: loop.id,
|
|
205
403
|
status: 413,
|
|
@@ -212,6 +410,9 @@ export default class AgentLoop {
|
|
|
212
410
|
};
|
|
213
411
|
}
|
|
214
412
|
|
|
413
|
+
console.error(
|
|
414
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} completed status=${result.status}`,
|
|
415
|
+
);
|
|
215
416
|
await this.#db.complete_loop.run({
|
|
216
417
|
id: loop.id,
|
|
217
418
|
status: result.status,
|
|
@@ -222,7 +423,7 @@ export default class AgentLoop {
|
|
|
222
423
|
const runRow = await this.#db.get_run_by_alias.get({
|
|
223
424
|
alias: currentAlias,
|
|
224
425
|
});
|
|
225
|
-
return { run: currentAlias, status: runRow
|
|
426
|
+
return { run: currentAlias, status: runRow.status };
|
|
226
427
|
} finally {
|
|
227
428
|
this.#activeRuns.delete(currentRunId);
|
|
228
429
|
}
|
|
@@ -241,16 +442,14 @@ export default class AgentLoop {
|
|
|
241
442
|
noInteraction,
|
|
242
443
|
noWeb,
|
|
243
444
|
noProposals,
|
|
445
|
+
yolo,
|
|
244
446
|
options,
|
|
245
447
|
hook,
|
|
246
448
|
signal,
|
|
247
449
|
}) {
|
|
248
450
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
249
451
|
if (runRow.status !== 102) {
|
|
250
|
-
await this.#
|
|
251
|
-
id: currentRunId,
|
|
252
|
-
status: 102,
|
|
253
|
-
});
|
|
452
|
+
await this.#setRunStatus(currentRunId, currentAlias, 102);
|
|
254
453
|
}
|
|
255
454
|
|
|
256
455
|
const modelContextSize =
|
|
@@ -266,20 +465,7 @@ export default class AgentLoop {
|
|
|
266
465
|
});
|
|
267
466
|
|
|
268
467
|
let loopIteration = 0;
|
|
269
|
-
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS)
|
|
270
|
-
const healer = new ResponseHealer();
|
|
271
|
-
|
|
272
|
-
let _lastAssembledTokens = 0;
|
|
273
|
-
let recovery = null; // { target, promptPath, strikes, lastTokens }
|
|
274
|
-
|
|
275
|
-
// Previous loop entries stay at full fidelity — the model is
|
|
276
|
-
// instructed to summarize and demote them. Budget enforcement
|
|
277
|
-
// catches overflow if the model fails to manage context.
|
|
278
|
-
|
|
279
|
-
// Restore any prompt entries left at summary fidelity by a recovery
|
|
280
|
-
// phase that was interrupted (server crash, restart). If the full
|
|
281
|
-
// prompt would overflow, Prompt Demotion on turn 1 handles it.
|
|
282
|
-
await this.#knownStore.restoreSummarizedPrompts(currentRunId);
|
|
468
|
+
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS);
|
|
283
469
|
|
|
284
470
|
await this.#hooks.loop.started.emit({
|
|
285
471
|
runId: currentRunId,
|
|
@@ -291,9 +477,17 @@ export default class AgentLoop {
|
|
|
291
477
|
try {
|
|
292
478
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
293
479
|
if (signal.aborted) {
|
|
294
|
-
|
|
295
|
-
|
|
480
|
+
console.error(
|
|
481
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} ABORT via signal`,
|
|
482
|
+
);
|
|
483
|
+
await this.#setRunStatus(currentRunId, currentAlias, 499);
|
|
484
|
+
await this.#emitRunState({
|
|
485
|
+
projectId,
|
|
486
|
+
runId: currentRunId,
|
|
487
|
+
alias: currentAlias,
|
|
488
|
+
turn: loopIteration,
|
|
296
489
|
status: 499,
|
|
490
|
+
contextSize,
|
|
297
491
|
});
|
|
298
492
|
const out = {
|
|
299
493
|
run: currentAlias,
|
|
@@ -304,6 +498,9 @@ export default class AgentLoop {
|
|
|
304
498
|
return out;
|
|
305
499
|
}
|
|
306
500
|
loopIteration++;
|
|
501
|
+
console.error(
|
|
502
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} ENTER (max=${MAX_LOOP_ITERATIONS})`,
|
|
503
|
+
);
|
|
307
504
|
|
|
308
505
|
let turnPrompt;
|
|
309
506
|
if (loopIteration === 1) {
|
|
@@ -315,6 +512,9 @@ export default class AgentLoop {
|
|
|
315
512
|
);
|
|
316
513
|
}
|
|
317
514
|
|
|
515
|
+
console.error(
|
|
516
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} executing turn`,
|
|
517
|
+
);
|
|
318
518
|
const result = await this.#turnExecutor.execute({
|
|
319
519
|
mode,
|
|
320
520
|
project,
|
|
@@ -326,198 +526,123 @@ export default class AgentLoop {
|
|
|
326
526
|
loopPrompt: turnPrompt,
|
|
327
527
|
loopIteration,
|
|
328
528
|
noRepo,
|
|
529
|
+
noWeb,
|
|
530
|
+
noInteraction,
|
|
531
|
+
noProposals,
|
|
532
|
+
yolo,
|
|
329
533
|
toolSet,
|
|
330
|
-
inRecovery: recovery !== null,
|
|
331
534
|
contextSize,
|
|
332
535
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
333
536
|
signal,
|
|
334
537
|
});
|
|
538
|
+
console.error(
|
|
539
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} turn done: status=${result.status} turn=${result.turn}`,
|
|
540
|
+
);
|
|
335
541
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
id: currentRunId,
|
|
344
|
-
status: 200,
|
|
345
|
-
});
|
|
346
|
-
const out = {
|
|
347
|
-
run: currentAlias,
|
|
348
|
-
status: 413,
|
|
349
|
-
overflow: result.overflow,
|
|
350
|
-
assembledTokens: result.assembledTokens,
|
|
351
|
-
contextSize: result.contextSize,
|
|
352
|
-
turn: result.turn,
|
|
353
|
-
};
|
|
354
|
-
await hook.completed.emit({ projectId, ...out });
|
|
355
|
-
return out;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
_lastAssembledTokens = result.assembledTokens;
|
|
359
|
-
|
|
360
|
-
// Budget recovery: enforce progress toward context target.
|
|
361
|
-
const ra = advanceRecovery(recovery, result);
|
|
362
|
-
recovery = ra.next;
|
|
363
|
-
if (ra.action === "restore" && ra.promptPath) {
|
|
364
|
-
await this.#knownStore.setFidelity(
|
|
365
|
-
currentRunId,
|
|
366
|
-
ra.promptPath,
|
|
367
|
-
"promoted",
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
if (ra.action === "hard413") {
|
|
371
|
-
await this.#db.update_run_status.run({
|
|
372
|
-
id: currentRunId,
|
|
373
|
-
status: 413,
|
|
374
|
-
});
|
|
375
|
-
const out = {
|
|
376
|
-
run: currentAlias,
|
|
377
|
-
status: 413,
|
|
378
|
-
turn: result.turn,
|
|
379
|
-
};
|
|
380
|
-
await hook.completed.emit({ projectId, ...out });
|
|
381
|
-
return out;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const runUsage = await this.#db.get_run_usage.get({
|
|
385
|
-
run_id: currentRunId,
|
|
386
|
-
});
|
|
387
|
-
const history = await this.#knownStore.getLog(currentRunId);
|
|
388
|
-
const unknowns = await this.#db.get_unknowns.all({
|
|
389
|
-
run_id: currentRunId,
|
|
542
|
+
const verdict = await this.#hooks.error.verdict({
|
|
543
|
+
store: this.#entries,
|
|
544
|
+
runId: currentRunId,
|
|
545
|
+
loopId: currentLoopId,
|
|
546
|
+
turn: result.turn,
|
|
547
|
+
recorded: result.recorded,
|
|
548
|
+
summaryText: result.summaryText,
|
|
390
549
|
});
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
550
|
+
const vStatus = verdict.status === undefined ? "-" : verdict.status;
|
|
551
|
+
const vReason = verdict.reason ? verdict.reason : "-";
|
|
552
|
+
console.error(
|
|
553
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} verdict: continue=${verdict.continue} status=${vStatus} reason=${vReason}`,
|
|
554
|
+
);
|
|
394
555
|
|
|
395
|
-
await this.#
|
|
556
|
+
await this.#emitRunState({
|
|
396
557
|
projectId,
|
|
397
|
-
|
|
558
|
+
runId: currentRunId,
|
|
559
|
+
alias: currentAlias,
|
|
398
560
|
turn: result.turn,
|
|
399
|
-
status: 102,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
403
|
-
telemetry: {
|
|
404
|
-
modelAlias: result.modelAlias,
|
|
405
|
-
model: result.model,
|
|
406
|
-
temperature: result.temperature,
|
|
407
|
-
context_size: result.contextSize,
|
|
408
|
-
context_tokens:
|
|
409
|
-
(
|
|
410
|
-
await this.#db.get_turn_context_tokens.get({
|
|
411
|
-
run_id: currentRunId,
|
|
412
|
-
sequence: result.turn,
|
|
413
|
-
})
|
|
414
|
-
)?.context_tokens ?? 0,
|
|
415
|
-
prompt_tokens: runUsage.prompt_tokens,
|
|
416
|
-
cached_tokens: runUsage.cached_tokens,
|
|
417
|
-
completion_tokens: runUsage.completion_tokens,
|
|
418
|
-
reasoning_tokens: runUsage.reasoning_tokens,
|
|
419
|
-
total_tokens: runUsage.total_tokens,
|
|
420
|
-
cost: runUsage.cost,
|
|
421
|
-
context_distribution: await this.#db.get_turn_distribution.all({
|
|
422
|
-
run_id: currentRunId,
|
|
423
|
-
turn: result.turn,
|
|
424
|
-
}),
|
|
425
|
-
},
|
|
561
|
+
status: verdict.continue ? 102 : verdict.status,
|
|
562
|
+
contextSize,
|
|
563
|
+
result,
|
|
426
564
|
});
|
|
427
565
|
await this.#hooks.run.step.completed.emit({
|
|
428
566
|
projectId,
|
|
429
567
|
run: currentAlias,
|
|
430
568
|
turn: result.turn,
|
|
431
|
-
flags: result.flags,
|
|
432
569
|
});
|
|
570
|
+
if (verdict.continue) continue;
|
|
433
571
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
await this.#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
});
|
|
443
|
-
const out = {
|
|
444
|
-
run: currentAlias,
|
|
445
|
-
status: 200,
|
|
572
|
+
console.error(
|
|
573
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} CLOSE status=${verdict.status} reason=${vReason}`,
|
|
574
|
+
);
|
|
575
|
+
await this.#setRunStatus(currentRunId, currentAlias, verdict.status);
|
|
576
|
+
if (verdict.reason) {
|
|
577
|
+
await this.#hooks.error.log.emit({
|
|
578
|
+
store: this.#entries,
|
|
579
|
+
runId: currentRunId,
|
|
446
580
|
turn: result.turn,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return out;
|
|
581
|
+
loopId: currentLoopId,
|
|
582
|
+
message: verdict.reason,
|
|
583
|
+
});
|
|
451
584
|
}
|
|
452
|
-
|
|
453
|
-
const progress = healer.assessProgress(result);
|
|
454
|
-
if (progress.continue) continue;
|
|
455
|
-
|
|
456
|
-
await this.#db.update_run_status.run({
|
|
457
|
-
id: currentRunId,
|
|
458
|
-
status: 200,
|
|
459
|
-
});
|
|
460
585
|
const out = {
|
|
461
586
|
run: currentAlias,
|
|
462
|
-
status:
|
|
587
|
+
status: verdict.status,
|
|
463
588
|
turn: result.turn,
|
|
589
|
+
reason: verdict.reason,
|
|
464
590
|
};
|
|
465
591
|
await hook.completed.emit({ projectId, ...out });
|
|
466
592
|
return out;
|
|
467
593
|
}
|
|
468
594
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
595
|
+
// MAX_TURNS exhaustion without a terminal update is abandonment.
|
|
596
|
+
console.error(
|
|
597
|
+
`[LOOP] ${currentAlias} hit MAX_LOOP_ITERATIONS=${MAX_LOOP_ITERATIONS} — abandoning at 499`,
|
|
598
|
+
);
|
|
599
|
+
await this.#setRunStatus(currentRunId, currentAlias, 499);
|
|
600
|
+
await this.#emitRunState({
|
|
601
|
+
projectId,
|
|
602
|
+
runId: currentRunId,
|
|
603
|
+
alias: currentAlias,
|
|
604
|
+
turn: loopIteration,
|
|
605
|
+
status: 499,
|
|
606
|
+
contextSize,
|
|
472
607
|
});
|
|
473
608
|
const out = {
|
|
474
609
|
run: currentAlias,
|
|
475
|
-
status:
|
|
610
|
+
status: 499,
|
|
476
611
|
turn: loopIteration,
|
|
477
612
|
};
|
|
478
613
|
await hook.completed.emit({ projectId, ...out });
|
|
479
614
|
return out;
|
|
480
615
|
} catch (err) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
616
|
+
const status = signal.aborted ? 499 : 500;
|
|
617
|
+
await this.#setRunStatus(currentRunId, currentAlias, status);
|
|
618
|
+
await this.#emitRunState({
|
|
619
|
+
projectId,
|
|
620
|
+
runId: currentRunId,
|
|
621
|
+
alias: currentAlias,
|
|
622
|
+
turn: loopIteration,
|
|
623
|
+
status,
|
|
624
|
+
contextSize,
|
|
625
|
+
});
|
|
626
|
+
if (status === 500) {
|
|
627
|
+
await this.#hooks.error.log.emit({
|
|
628
|
+
store: this.#entries,
|
|
629
|
+
runId: currentRunId,
|
|
630
|
+
turn: loopIteration,
|
|
631
|
+
loopId: currentLoopId,
|
|
632
|
+
message: `${err.message}\n${err.stack}`,
|
|
485
633
|
});
|
|
486
|
-
return { run: currentAlias, status: 499, turn: loopIteration };
|
|
487
634
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
await this.#db.update_run_status.run({
|
|
491
|
-
id: currentRunId,
|
|
492
|
-
status: 500,
|
|
493
|
-
});
|
|
494
|
-
try {
|
|
495
|
-
await this.#knownStore.upsert(
|
|
496
|
-
currentRunId,
|
|
497
|
-
loopIteration,
|
|
498
|
-
`error://${loopIteration}`,
|
|
499
|
-
`${err.message}\n${err.stack}`,
|
|
500
|
-
500,
|
|
501
|
-
{ loopId: currentLoopId },
|
|
502
|
-
);
|
|
503
|
-
} catch {}
|
|
504
|
-
const out = {
|
|
505
|
-
run: currentAlias,
|
|
506
|
-
status: 500,
|
|
507
|
-
turn: loopIteration,
|
|
508
|
-
error: err.message,
|
|
509
|
-
};
|
|
635
|
+
const out = { run: currentAlias, status, turn: loopIteration };
|
|
636
|
+
if (status === 500) out.error = err.message;
|
|
510
637
|
await hook.completed.emit({ projectId, ...out });
|
|
511
638
|
return out;
|
|
512
639
|
} finally {
|
|
513
|
-
await this.#hooks.loop.completed
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
})
|
|
520
|
-
.catch(() => {});
|
|
640
|
+
await this.#hooks.loop.completed.emit({
|
|
641
|
+
runId: currentRunId,
|
|
642
|
+
loopId: currentLoopId,
|
|
643
|
+
mode,
|
|
644
|
+
turns: loopIteration,
|
|
645
|
+
});
|
|
521
646
|
}
|
|
522
647
|
}
|
|
523
648
|
|
|
@@ -529,119 +654,128 @@ export default class AgentLoop {
|
|
|
529
654
|
|
|
530
655
|
const { path, action, output } = resolution;
|
|
531
656
|
|
|
532
|
-
if (action
|
|
533
|
-
|
|
534
|
-
|
|
657
|
+
if (action !== "accept" && action !== "error" && action !== "reject") {
|
|
658
|
+
throw new Error(msg("error.resolution_invalid", { action }));
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (action === "reject") {
|
|
662
|
+
await this.#entries.set({
|
|
663
|
+
runId,
|
|
664
|
+
path,
|
|
665
|
+
state: "failed",
|
|
666
|
+
body: output ? output : "rejected",
|
|
667
|
+
outcome: "permission",
|
|
668
|
+
});
|
|
669
|
+
await this.#hooks.proposal.rejected.emit({
|
|
535
670
|
runId,
|
|
671
|
+
runRow,
|
|
536
672
|
path,
|
|
537
|
-
attrs,
|
|
538
673
|
output,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
}
|
|
674
|
+
db: this.#db,
|
|
675
|
+
entries: this.#entries,
|
|
676
|
+
});
|
|
677
|
+
// Report the CURRENT run status (typically 102 mid-run) so the
|
|
678
|
+
// client's dispatch handler doesn't mistake a successful
|
|
679
|
+
// resolve's HTTP-style 200 ack for a terminal run status and
|
|
680
|
+
// prematurely close the document. Real terminal state comes
|
|
681
|
+
// from the run/state notification at end-of-turn.
|
|
682
|
+
return { run: runAlias, status: runRow.status };
|
|
683
|
+
}
|
|
551
684
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
685
|
+
const attrs = await this.#entries.getAttributes(runId, path);
|
|
686
|
+
const project = await this.#db.get_project_by_id.get({
|
|
687
|
+
id: runRow.project_id,
|
|
688
|
+
});
|
|
689
|
+
const ctx = {
|
|
690
|
+
runId,
|
|
691
|
+
runRow,
|
|
692
|
+
projectId: runRow.project_id,
|
|
693
|
+
projectRoot: project?.project_root,
|
|
694
|
+
path,
|
|
695
|
+
attrs,
|
|
696
|
+
output,
|
|
697
|
+
db: this.#db,
|
|
698
|
+
entries: this.#entries,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Plugins veto acceptance (e.g. readonly) via proposal.accepting.
|
|
702
|
+
// First veto wins: state=failed with plugin-supplied outcome + body.
|
|
703
|
+
if (action === "accept") {
|
|
704
|
+
const veto = await this.#hooks.proposal.accepting.filter(null, ctx);
|
|
705
|
+
if (veto?.allow === false) {
|
|
706
|
+
await this.#entries.set({
|
|
707
|
+
runId,
|
|
708
|
+
path,
|
|
709
|
+
state: "failed",
|
|
710
|
+
outcome: veto.outcome,
|
|
711
|
+
body: veto.body,
|
|
556
712
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
|
|
560
|
-
const fileBody = await this.#knownStore.getBody(runId, attrs.file);
|
|
561
|
-
if (fileBody != null) {
|
|
562
|
-
const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
|
|
563
|
-
let patched = fileBody;
|
|
564
|
-
for (const block of blocks) {
|
|
565
|
-
const m = block.match(
|
|
566
|
-
/<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
|
|
567
|
-
);
|
|
568
|
-
if (m) patched = patched.replace(m[1], m[2]);
|
|
569
|
-
}
|
|
570
|
-
const turn = (await this.#db.get_run_by_id.get({ id: runId }))
|
|
571
|
-
.next_turn;
|
|
572
|
-
await this.#knownStore.upsert(
|
|
573
|
-
runId,
|
|
574
|
-
turn,
|
|
575
|
-
attrs.file,
|
|
576
|
-
patched,
|
|
577
|
-
200,
|
|
578
|
-
);
|
|
579
|
-
// Write patched content to disk
|
|
580
|
-
if (projectRoot) {
|
|
581
|
-
const { writeFile } = await import("node:fs/promises");
|
|
582
|
-
const { join } = await import("node:path");
|
|
583
|
-
await writeFile(join(projectRoot, attrs.file), patched).catch(
|
|
584
|
-
() => {},
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (path.startsWith("rm://")) {
|
|
591
|
-
if (attrs?.path) {
|
|
592
|
-
await this.#knownStore.remove(runId, attrs.path);
|
|
593
|
-
if (projectRoot) {
|
|
594
|
-
const { unlink } = await import("node:fs/promises");
|
|
595
|
-
const { join } = await import("node:path");
|
|
596
|
-
await unlink(join(projectRoot, attrs.path)).catch(() => {});
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
if (path.startsWith("mv://")) {
|
|
602
|
-
if (attrs?.isMove && attrs?.from) {
|
|
603
|
-
await this.#knownStore.remove(runId, attrs.from);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
713
|
+
return { ok: true, state: "failed", outcome: veto.outcome };
|
|
606
714
|
}
|
|
607
|
-
} else if (action === "reject") {
|
|
608
|
-
await this.#knownStore.resolve(runId, path, 403, output || "rejected");
|
|
609
|
-
} else {
|
|
610
|
-
throw new Error(msg("error.resolution_invalid", { action }));
|
|
611
715
|
}
|
|
612
716
|
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
717
|
+
// Compose the resolved body. Default is output || "". Plugins may
|
|
718
|
+
// override via proposal.content (e.g. set prefers the existing
|
|
719
|
+
// proposed body from the log entry).
|
|
720
|
+
const defaultBody = output ? output : "";
|
|
721
|
+
const resolvedBody = await this.#hooks.proposal.content.filter(
|
|
722
|
+
defaultBody,
|
|
723
|
+
ctx,
|
|
724
|
+
);
|
|
725
|
+
const state = action === "error" ? "failed" : "resolved";
|
|
726
|
+
const outcome = action === "error" ? "error" : null;
|
|
727
|
+
const existing = await this.#entries.getState(runId, path);
|
|
728
|
+
const existingTurn = existing?.turn === undefined ? 0 : existing.turn;
|
|
729
|
+
await this.#entries.set({
|
|
730
|
+
runId,
|
|
731
|
+
turn: existingTurn,
|
|
732
|
+
path,
|
|
733
|
+
state,
|
|
734
|
+
body: resolvedBody,
|
|
735
|
+
outcome,
|
|
736
|
+
});
|
|
617
737
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
738
|
+
const event =
|
|
739
|
+
action === "accept"
|
|
740
|
+
? this.#hooks.proposal.accepted
|
|
741
|
+
: this.#hooks.proposal.rejected;
|
|
742
|
+
await event.emit({ ...ctx, resolvedBody });
|
|
743
|
+
|
|
744
|
+
// Same rationale as the reject path: return current run status
|
|
745
|
+
// (102 mid-run) rather than a hardcoded 200 so the nvim client
|
|
746
|
+
// doesn't treat the RPC ack as a terminal signal.
|
|
747
|
+
return { run: runAlias, status: runRow.status };
|
|
628
748
|
}
|
|
629
749
|
|
|
630
|
-
async inject(runAlias, message) {
|
|
750
|
+
async inject(runAlias, message, mode, options = {}) {
|
|
751
|
+
if (mode !== "ask" && mode !== "act") {
|
|
752
|
+
throw new Error(
|
|
753
|
+
`inject: mode is required and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
631
756
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
632
757
|
if (!runRow)
|
|
633
758
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
634
759
|
|
|
760
|
+
const noRepo = options?.noRepo ?? process.env.RUMMY_NO_REPO === "1";
|
|
761
|
+
const noInteraction =
|
|
762
|
+
options?.noInteraction ?? process.env.RUMMY_NO_INTERACTION === "1";
|
|
763
|
+
const noWeb = options?.noWeb ?? process.env.RUMMY_NO_WEB === "1";
|
|
764
|
+
const noProposals =
|
|
765
|
+
options?.noProposals ?? process.env.RUMMY_NO_PROPOSALS === "1";
|
|
766
|
+
const yolo = options?.yolo ?? process.env.RUMMY_YOLO === "1";
|
|
767
|
+
|
|
635
768
|
const nextTurn = runRow.next_turn;
|
|
636
769
|
|
|
637
|
-
await this.#
|
|
638
|
-
runRow.id,
|
|
639
|
-
nextTurn,
|
|
640
|
-
`prompt://${nextTurn}`,
|
|
641
|
-
message,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
770
|
+
await this.#entries.set({
|
|
771
|
+
runId: runRow.id,
|
|
772
|
+
turn: nextTurn,
|
|
773
|
+
path: `prompt://${nextTurn}`,
|
|
774
|
+
body: message,
|
|
775
|
+
state: "resolved",
|
|
776
|
+
attributes: { mode },
|
|
777
|
+
writer: "plugin",
|
|
778
|
+
});
|
|
645
779
|
|
|
646
780
|
if (this.#activeRuns.has(runRow.id)) {
|
|
647
781
|
return { run: runAlias, status: runRow.status, injected: "next_turn" };
|
|
@@ -651,31 +785,38 @@ export default class AgentLoop {
|
|
|
651
785
|
await this.#db.enqueue_loop.get({
|
|
652
786
|
run_id: runRow.id,
|
|
653
787
|
sequence: injectLoopSeq.sequence,
|
|
654
|
-
mode
|
|
788
|
+
mode,
|
|
655
789
|
model: runRow.model,
|
|
656
790
|
prompt: message,
|
|
657
|
-
config:
|
|
791
|
+
config: JSON.stringify({
|
|
792
|
+
noRepo,
|
|
793
|
+
noInteraction,
|
|
794
|
+
noWeb,
|
|
795
|
+
noProposals,
|
|
796
|
+
yolo,
|
|
797
|
+
temperature: options?.temperature,
|
|
798
|
+
}),
|
|
658
799
|
});
|
|
659
800
|
|
|
660
801
|
const projectId = runRow.project_id;
|
|
661
802
|
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
662
|
-
|
|
803
|
+
const controller = new AbortController();
|
|
804
|
+
const promise = this.#drainQueue(
|
|
805
|
+
runRow.id,
|
|
806
|
+
runAlias,
|
|
807
|
+
projectId,
|
|
808
|
+
project,
|
|
809
|
+
{},
|
|
810
|
+
controller,
|
|
811
|
+
);
|
|
812
|
+
this.#activeRuns.set(runRow.id, { controller, promise });
|
|
813
|
+
return promise;
|
|
663
814
|
}
|
|
664
815
|
|
|
665
816
|
async getRunHistory(runAlias) {
|
|
666
817
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
667
818
|
if (!runRow)
|
|
668
819
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
669
|
-
return this.#
|
|
820
|
+
return this.#entries.getLog(runRow.id);
|
|
670
821
|
}
|
|
671
822
|
}
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Pure recovery state transition — exported for testing.
|
|
675
|
-
*
|
|
676
|
-
* @param {object|null} recovery Current recovery state (mutated copy returned).
|
|
677
|
-
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
678
|
-
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
679
|
-
*/
|
|
680
|
-
// Re-export for backward compatibility with tests
|
|
681
|
-
export { advanceRecovery } from "../plugins/budget/recovery.js";
|