@possumtech/rummy 0.3.0 → 0.4.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 +13 -1
- package/PLUGINS.md +1 -1
- package/README.md +5 -1
- package/SPEC.md +211 -54
- package/migrations/001_initial_schema.sql +3 -4
- package/package.json +7 -3
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +183 -238
- package/src/agent/ContextAssembler.js +2 -0
- package/src/agent/KnownStore.js +36 -85
- package/src/agent/ResponseHealer.js +65 -31
- package/src/agent/TurnExecutor.js +284 -382
- package/src/agent/XmlParser.js +28 -4
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +32 -34
- package/src/agent/runs.sql +2 -2
- package/src/agent/tokens.js +1 -0
- package/src/agent/turns.sql +5 -0
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +2 -4
- package/src/hooks/ToolRegistry.js +8 -13
- package/src/plugins/ask_user/ask_userDoc.js +3 -8
- package/src/plugins/budget/README.md +26 -30
- package/src/plugins/budget/budget.js +69 -36
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cp.js +1 -1
- package/src/plugins/cp/cpDoc.js +5 -10
- package/src/plugins/env/envDoc.js +3 -8
- package/src/plugins/get/get.js +70 -2
- package/src/plugins/get/getDoc.js +19 -16
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +3 -2
- package/src/plugins/instructions/preamble.md +33 -12
- package/src/plugins/known/known.js +66 -17
- package/src/plugins/known/knownDoc.js +7 -10
- package/src/plugins/mv/mv.js +18 -1
- package/src/plugins/mv/mvDoc.js +9 -10
- package/src/plugins/{current → performed}/README.md +4 -3
- package/src/plugins/{current/current.js → performed/performed.js} +15 -20
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/previous/README.md +2 -1
- package/src/plugins/previous/previous.js +31 -25
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +10 -60
- package/src/plugins/prompt/prompt.js +10 -8
- package/src/plugins/rm/rm.js +27 -15
- package/src/plugins/rm/rmDoc.js +6 -11
- package/src/plugins/rpc/rpc.js +3 -1
- package/src/plugins/set/set.js +125 -92
- package/src/plugins/set/setDoc.js +28 -37
- package/src/plugins/sh/shDoc.js +2 -7
- package/src/plugins/summarize/summarize.js +7 -0
- package/src/plugins/summarize/summarizeDoc.js +6 -11
- package/src/plugins/telemetry/telemetry.js +14 -9
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +18 -0
- package/src/plugins/unknown/README.md +2 -1
- package/src/plugins/unknown/unknown.js +26 -4
- package/src/plugins/unknown/unknownDoc.js +9 -14
- package/src/plugins/update/update.js +7 -0
- package/src/plugins/update/updateDoc.js +6 -11
- package/src/server/ClientConnection.js +69 -45
- package/src/sql/v_model_context.sql +7 -17
- package/src/plugins/budget/BudgetGuard.js +0 -74
package/src/agent/AgentLoop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { advanceRecovery } from "../plugins/budget/recovery.js";
|
|
1
2
|
import KnownStore from "./KnownStore.js";
|
|
2
3
|
import msg from "./messages.js";
|
|
3
4
|
import ResponseHealer from "./ResponseHealer.js";
|
|
@@ -70,14 +71,15 @@ export default class AgentLoop {
|
|
|
70
71
|
const existing = this.#activeRuns.get(existingRun.id);
|
|
71
72
|
if (existing) existing.abort();
|
|
72
73
|
|
|
74
|
+
// Clean up stale proposals from interrupted runs
|
|
73
75
|
const unresolved = await this.#knownStore.getUnresolved(existingRun.id);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
for (const u of unresolved) {
|
|
77
|
+
await this.#knownStore.resolve(
|
|
78
|
+
existingRun.id,
|
|
79
|
+
u.path,
|
|
80
|
+
499,
|
|
81
|
+
"Stale proposal from interrupted run",
|
|
82
|
+
);
|
|
81
83
|
}
|
|
82
84
|
return { runId: existingRun.id, alias: existingRun.alias };
|
|
83
85
|
}
|
|
@@ -121,18 +123,10 @@ export default class AgentLoop {
|
|
|
121
123
|
const noRepo = options?.noRepo === true;
|
|
122
124
|
const noInteraction = options?.noInteraction === true;
|
|
123
125
|
const noWeb = options?.noWeb === true;
|
|
126
|
+
const noProposals = options?.noProposals === true;
|
|
124
127
|
const requestedModel = model;
|
|
125
128
|
|
|
126
129
|
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
127
|
-
if (runInfo.blocked) {
|
|
128
|
-
return {
|
|
129
|
-
run: runInfo.alias,
|
|
130
|
-
status: 202,
|
|
131
|
-
remainingCount: runInfo.proposed.length,
|
|
132
|
-
proposed: runInfo.proposed,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
130
|
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
137
131
|
|
|
138
132
|
const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
@@ -146,6 +140,7 @@ export default class AgentLoop {
|
|
|
146
140
|
noRepo,
|
|
147
141
|
noInteraction,
|
|
148
142
|
noWeb,
|
|
143
|
+
noProposals,
|
|
149
144
|
temperature: options?.temperature,
|
|
150
145
|
}),
|
|
151
146
|
});
|
|
@@ -164,100 +159,74 @@ export default class AgentLoop {
|
|
|
164
159
|
}
|
|
165
160
|
|
|
166
161
|
async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
while (true) {
|
|
170
|
-
const loop = await this.#db.claim_next_loop.get({
|
|
171
|
-
run_id: currentRunId,
|
|
172
|
-
});
|
|
173
|
-
if (!loop) break;
|
|
174
|
-
|
|
175
|
-
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
176
|
-
const hook =
|
|
177
|
-
loop.mode === "panic"
|
|
178
|
-
? this.#hooks.panic
|
|
179
|
-
: loop.mode === "ask"
|
|
180
|
-
? this.#hooks.ask
|
|
181
|
-
: this.#hooks.act;
|
|
182
|
-
|
|
183
|
-
const result = await this.#executeLoop({
|
|
184
|
-
mode: loop.mode,
|
|
185
|
-
project,
|
|
186
|
-
projectId,
|
|
187
|
-
currentRunId,
|
|
188
|
-
currentAlias,
|
|
189
|
-
currentLoopId: loop.id,
|
|
190
|
-
requestedModel: loop.model,
|
|
191
|
-
prompt: loop.prompt,
|
|
192
|
-
noRepo: loopConfig.noRepo || false,
|
|
193
|
-
noInteraction: loopConfig.noInteraction || false,
|
|
194
|
-
noWeb: loopConfig.noWeb || false,
|
|
195
|
-
options: { ...options, temperature: loopConfig.temperature },
|
|
196
|
-
hook,
|
|
197
|
-
});
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
this.#activeRuns.set(currentRunId, controller);
|
|
198
164
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
result: JSON.stringify(result),
|
|
165
|
+
try {
|
|
166
|
+
while (true) {
|
|
167
|
+
const loop = await this.#db.claim_next_loop.get({
|
|
168
|
+
run_id: currentRunId,
|
|
204
169
|
});
|
|
170
|
+
if (!loop) break;
|
|
171
|
+
|
|
172
|
+
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
173
|
+
const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
174
|
+
|
|
175
|
+
let result;
|
|
176
|
+
try {
|
|
177
|
+
result = await this.#executeLoop({
|
|
178
|
+
mode: loop.mode,
|
|
179
|
+
project,
|
|
180
|
+
projectId,
|
|
181
|
+
currentRunId,
|
|
182
|
+
currentAlias,
|
|
183
|
+
currentLoopId: loop.id,
|
|
184
|
+
requestedModel: loop.model,
|
|
185
|
+
prompt: loop.prompt,
|
|
186
|
+
noRepo: loopConfig.noRepo || false,
|
|
187
|
+
noInteraction: loopConfig.noInteraction || false,
|
|
188
|
+
noWeb: loopConfig.noWeb || false,
|
|
189
|
+
noProposals: loopConfig.noProposals || false,
|
|
190
|
+
options: { ...options, temperature: loopConfig.temperature },
|
|
191
|
+
hook,
|
|
192
|
+
signal: controller.signal,
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
await this.#db.complete_loop.run({
|
|
196
|
+
id: loop.id,
|
|
197
|
+
status: 500,
|
|
198
|
+
result: JSON.stringify({ error: err.message }),
|
|
199
|
+
});
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
205
202
|
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
if (result.status === 413) {
|
|
204
|
+
await this.#db.complete_loop.run({
|
|
205
|
+
id: loop.id,
|
|
206
|
+
status: 413,
|
|
207
|
+
result: JSON.stringify(result),
|
|
208
|
+
});
|
|
208
209
|
return {
|
|
209
210
|
run: currentAlias,
|
|
210
211
|
status: 413,
|
|
211
|
-
error:
|
|
212
|
-
loop.mode === "panic"
|
|
213
|
-
? `Panic mode failed to free enough space (${result.overflow} tokens over).`
|
|
214
|
-
: `Context full (${result.overflow} tokens over).`,
|
|
212
|
+
error: `Context full (${result.overflow} tokens over).`,
|
|
215
213
|
};
|
|
216
214
|
}
|
|
217
215
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
contextSize: result.contextSize,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Enqueue panic loop
|
|
226
|
-
const panicSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
227
|
-
await this.#db.enqueue_loop.get({
|
|
228
|
-
run_id: currentRunId,
|
|
229
|
-
sequence: panicSeq.sequence,
|
|
230
|
-
mode: "panic",
|
|
231
|
-
model: loop.model,
|
|
232
|
-
prompt: panicPrompt,
|
|
233
|
-
config: JSON.stringify({ noRepo: true }),
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// Re-enqueue the original loop to retry after panic
|
|
237
|
-
const retrySeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
238
|
-
await this.#db.enqueue_loop.get({
|
|
239
|
-
run_id: currentRunId,
|
|
240
|
-
sequence: retrySeq.sequence,
|
|
241
|
-
mode: loop.mode,
|
|
242
|
-
model: loop.model,
|
|
243
|
-
prompt: loop.prompt,
|
|
244
|
-
config: loop.config,
|
|
216
|
+
await this.#db.complete_loop.run({
|
|
217
|
+
id: loop.id,
|
|
218
|
+
status: result.status,
|
|
219
|
+
result: JSON.stringify(result),
|
|
245
220
|
});
|
|
246
|
-
|
|
247
|
-
continue;
|
|
248
221
|
}
|
|
249
222
|
|
|
250
|
-
await this.#db.
|
|
251
|
-
|
|
252
|
-
status: result.status === 202 ? 202 : result.status,
|
|
253
|
-
result: JSON.stringify(result),
|
|
223
|
+
const runRow = await this.#db.get_run_by_alias.get({
|
|
224
|
+
alias: currentAlias,
|
|
254
225
|
});
|
|
255
|
-
|
|
256
|
-
|
|
226
|
+
return { run: currentAlias, status: runRow?.status ?? 200 };
|
|
227
|
+
} finally {
|
|
228
|
+
this.#activeRuns.delete(currentRunId);
|
|
257
229
|
}
|
|
258
|
-
|
|
259
|
-
const runRow = await this.#db.get_run_by_alias.get({ alias: currentAlias });
|
|
260
|
-
return { run: currentAlias, status: runRow?.status ?? 200 };
|
|
261
230
|
}
|
|
262
231
|
|
|
263
232
|
async #executeLoop({
|
|
@@ -272,8 +241,10 @@ export default class AgentLoop {
|
|
|
272
241
|
noRepo,
|
|
273
242
|
noInteraction,
|
|
274
243
|
noWeb,
|
|
244
|
+
noProposals,
|
|
275
245
|
options,
|
|
276
246
|
hook,
|
|
247
|
+
signal,
|
|
277
248
|
}) {
|
|
278
249
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
279
250
|
if (runRow.status !== 102) {
|
|
@@ -292,18 +263,24 @@ export default class AgentLoop {
|
|
|
292
263
|
const toolSet = this.#hooks.tools.resolveForLoop(mode, {
|
|
293
264
|
noInteraction,
|
|
294
265
|
noWeb,
|
|
266
|
+
noProposals,
|
|
295
267
|
});
|
|
296
268
|
|
|
297
269
|
let loopIteration = 0;
|
|
298
270
|
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
|
|
299
271
|
const healer = new ResponseHealer();
|
|
300
272
|
|
|
301
|
-
const controller = new AbortController();
|
|
302
|
-
this.#activeRuns.set(currentRunId, controller);
|
|
303
|
-
|
|
304
273
|
let _lastAssembledTokens = 0;
|
|
305
|
-
let
|
|
306
|
-
|
|
274
|
+
let recovery = null; // { target, promptPath, strikes, lastTokens }
|
|
275
|
+
|
|
276
|
+
// Previous loop entries stay at full fidelity — the model is
|
|
277
|
+
// instructed to summarize and demote them. Budget enforcement
|
|
278
|
+
// catches overflow if the model fails to manage context.
|
|
279
|
+
|
|
280
|
+
// Restore any prompt entries left at summary fidelity by a recovery
|
|
281
|
+
// phase that was interrupted (server crash, restart). If the full
|
|
282
|
+
// prompt would overflow, Prompt Demotion on turn 1 handles it.
|
|
283
|
+
await this.#knownStore.restoreSummarizedPrompts(currentRunId);
|
|
307
284
|
|
|
308
285
|
await this.#hooks.loop.started.emit({
|
|
309
286
|
runId: currentRunId,
|
|
@@ -314,7 +291,7 @@ export default class AgentLoop {
|
|
|
314
291
|
|
|
315
292
|
try {
|
|
316
293
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
317
|
-
if (
|
|
294
|
+
if (signal.aborted) {
|
|
318
295
|
await this.#db.update_run_status.run({
|
|
319
296
|
id: currentRunId,
|
|
320
297
|
status: 499,
|
|
@@ -332,8 +309,6 @@ export default class AgentLoop {
|
|
|
332
309
|
let turnPrompt;
|
|
333
310
|
if (loopIteration === 1) {
|
|
334
311
|
turnPrompt = prompt;
|
|
335
|
-
} else if (mode === "panic") {
|
|
336
|
-
turnPrompt = "Continue freeing space. Check <knowns> token counts.";
|
|
337
312
|
} else {
|
|
338
313
|
turnPrompt = this.#buildContinuationPrompt(
|
|
339
314
|
loopIteration,
|
|
@@ -350,16 +325,26 @@ export default class AgentLoop {
|
|
|
350
325
|
currentLoopId,
|
|
351
326
|
requestedModel,
|
|
352
327
|
loopPrompt: turnPrompt,
|
|
328
|
+
loopIteration,
|
|
353
329
|
noRepo,
|
|
354
330
|
toolSet,
|
|
331
|
+
inRecovery: recovery !== null,
|
|
355
332
|
contextSize,
|
|
356
333
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
357
|
-
signal
|
|
334
|
+
signal,
|
|
358
335
|
});
|
|
359
336
|
|
|
360
|
-
// Budget overflow — return 413 to drainQueue for panic mode
|
|
361
337
|
if (result.status === 413) {
|
|
362
|
-
|
|
338
|
+
await this.#db.complete_loop.run({
|
|
339
|
+
id: currentLoopId,
|
|
340
|
+
status: 413,
|
|
341
|
+
result: null,
|
|
342
|
+
});
|
|
343
|
+
await this.#db.update_run_status.run({
|
|
344
|
+
id: currentRunId,
|
|
345
|
+
status: 200,
|
|
346
|
+
});
|
|
347
|
+
const out = {
|
|
363
348
|
run: currentAlias,
|
|
364
349
|
status: 413,
|
|
365
350
|
overflow: result.overflow,
|
|
@@ -367,48 +352,34 @@ export default class AgentLoop {
|
|
|
367
352
|
contextSize: result.contextSize,
|
|
368
353
|
turn: result.turn,
|
|
369
354
|
};
|
|
355
|
+
await hook.completed.emit({ projectId, ...out });
|
|
356
|
+
return out;
|
|
370
357
|
}
|
|
371
358
|
|
|
372
359
|
_lastAssembledTokens = result.assembledTokens;
|
|
373
360
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
await this.#db.update_run_status.run({
|
|
397
|
-
id: currentRunId,
|
|
398
|
-
status: 200,
|
|
399
|
-
});
|
|
400
|
-
return {
|
|
401
|
-
run: currentAlias,
|
|
402
|
-
status: 413,
|
|
403
|
-
overflow: result.assembledTokens - contextSize,
|
|
404
|
-
assembledTokens: result.assembledTokens,
|
|
405
|
-
contextSize,
|
|
406
|
-
turn: result.turn,
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
_lastPanicTokens = result.assembledTokens;
|
|
361
|
+
// Budget recovery: enforce progress toward context target.
|
|
362
|
+
const ra = advanceRecovery(recovery, result);
|
|
363
|
+
recovery = ra.next;
|
|
364
|
+
if (ra.action === "restore" && ra.promptPath) {
|
|
365
|
+
await this.#knownStore.setFidelity(
|
|
366
|
+
currentRunId,
|
|
367
|
+
ra.promptPath,
|
|
368
|
+
"full",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (ra.action === "hard413") {
|
|
372
|
+
await this.#db.update_run_status.run({
|
|
373
|
+
id: currentRunId,
|
|
374
|
+
status: 413,
|
|
375
|
+
});
|
|
376
|
+
const out = {
|
|
377
|
+
run: currentAlias,
|
|
378
|
+
status: 413,
|
|
379
|
+
turn: result.turn,
|
|
380
|
+
};
|
|
381
|
+
await hook.completed.emit({ projectId, ...out });
|
|
382
|
+
return out;
|
|
412
383
|
}
|
|
413
384
|
|
|
414
385
|
const runUsage = await this.#db.get_run_usage.get({
|
|
@@ -418,8 +389,6 @@ export default class AgentLoop {
|
|
|
418
389
|
const unknowns = await this.#db.get_unknowns.all({
|
|
419
390
|
run_id: currentRunId,
|
|
420
391
|
});
|
|
421
|
-
const unresolved = await this.#knownStore.getUnresolved(currentRunId);
|
|
422
|
-
|
|
423
392
|
const latestSummary = history
|
|
424
393
|
.filter((e) => e.status === 200 && e.path?.startsWith("summarize://"))
|
|
425
394
|
.at(-1);
|
|
@@ -428,26 +397,22 @@ export default class AgentLoop {
|
|
|
428
397
|
projectId,
|
|
429
398
|
run: currentAlias,
|
|
430
399
|
turn: result.turn,
|
|
431
|
-
status:
|
|
400
|
+
status: 102,
|
|
432
401
|
summary: latestSummary?.body || "",
|
|
433
402
|
history,
|
|
434
403
|
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
435
|
-
proposed: unresolved.map((p) => ({
|
|
436
|
-
path: p.path,
|
|
437
|
-
type: KnownStore.toolFromPath(p.path) || "unknown",
|
|
438
|
-
attributes: p.attributes ? JSON.parse(p.attributes) : null,
|
|
439
|
-
})),
|
|
440
404
|
telemetry: {
|
|
441
405
|
modelAlias: result.modelAlias,
|
|
442
406
|
model: result.model,
|
|
443
407
|
temperature: result.temperature,
|
|
444
408
|
context_size: result.contextSize,
|
|
445
|
-
context_tokens:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
409
|
+
context_tokens:
|
|
410
|
+
(
|
|
411
|
+
await this.#db.get_turn_context_tokens.get({
|
|
412
|
+
run_id: currentRunId,
|
|
413
|
+
sequence: result.turn,
|
|
414
|
+
})
|
|
415
|
+
)?.context_tokens ?? 0,
|
|
451
416
|
prompt_tokens: runUsage.prompt_tokens,
|
|
452
417
|
cached_tokens: runUsage.cached_tokens,
|
|
453
418
|
completion_tokens: runUsage.completion_tokens,
|
|
@@ -460,21 +425,6 @@ export default class AgentLoop {
|
|
|
460
425
|
}),
|
|
461
426
|
},
|
|
462
427
|
});
|
|
463
|
-
if (unresolved.length > 0) {
|
|
464
|
-
await this.#db.update_run_status.run({
|
|
465
|
-
id: currentRunId,
|
|
466
|
-
status: 202,
|
|
467
|
-
});
|
|
468
|
-
const out = {
|
|
469
|
-
run: currentAlias,
|
|
470
|
-
status: 202,
|
|
471
|
-
turn: result.turn,
|
|
472
|
-
proposed: unresolved,
|
|
473
|
-
};
|
|
474
|
-
await hook.completed.emit({ projectId, ...out });
|
|
475
|
-
return out;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
428
|
await this.#hooks.run.step.completed.emit({
|
|
479
429
|
projectId,
|
|
480
430
|
run: currentAlias,
|
|
@@ -482,6 +432,9 @@ export default class AgentLoop {
|
|
|
482
432
|
flags: result.flags,
|
|
483
433
|
});
|
|
484
434
|
|
|
435
|
+
// Don't exit while budget recovery is still active.
|
|
436
|
+
if (recovery !== null) continue;
|
|
437
|
+
|
|
485
438
|
const repetition = healer.assessRepetition(result);
|
|
486
439
|
if (!repetition.continue) {
|
|
487
440
|
await this.#db.update_run_status.run({
|
|
@@ -526,7 +479,7 @@ export default class AgentLoop {
|
|
|
526
479
|
await hook.completed.emit({ projectId, ...out });
|
|
527
480
|
return out;
|
|
528
481
|
} catch (err) {
|
|
529
|
-
if (
|
|
482
|
+
if (signal.aborted) {
|
|
530
483
|
await this.#db.update_run_status.run({
|
|
531
484
|
id: currentRunId,
|
|
532
485
|
status: 499,
|
|
@@ -558,7 +511,6 @@ export default class AgentLoop {
|
|
|
558
511
|
await hook.completed.emit({ projectId, ...out });
|
|
559
512
|
return out;
|
|
560
513
|
} finally {
|
|
561
|
-
this.#activeRuns.delete(currentRunId);
|
|
562
514
|
await this.#hooks.loop.completed
|
|
563
515
|
.emit({
|
|
564
516
|
runId: currentRunId,
|
|
@@ -599,9 +551,51 @@ export default class AgentLoop {
|
|
|
599
551
|
}
|
|
600
552
|
|
|
601
553
|
if (action === "accept") {
|
|
554
|
+
const projectId = runRow.project_id;
|
|
555
|
+
const project = await this.#db.get_project_by_id.get({
|
|
556
|
+
id: projectId,
|
|
557
|
+
});
|
|
558
|
+
const projectRoot = project?.project_root;
|
|
559
|
+
|
|
560
|
+
if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
|
|
561
|
+
const fileBody = await this.#knownStore.getBody(runId, attrs.file);
|
|
562
|
+
if (fileBody != null) {
|
|
563
|
+
const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
|
|
564
|
+
let patched = fileBody;
|
|
565
|
+
for (const block of blocks) {
|
|
566
|
+
const m = block.match(
|
|
567
|
+
/<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
|
|
568
|
+
);
|
|
569
|
+
if (m) patched = patched.replace(m[1], m[2]);
|
|
570
|
+
}
|
|
571
|
+
const turn = (await this.#db.get_run_by_id.get({ id: runId }))
|
|
572
|
+
.next_turn;
|
|
573
|
+
await this.#knownStore.upsert(
|
|
574
|
+
runId,
|
|
575
|
+
turn,
|
|
576
|
+
attrs.file,
|
|
577
|
+
patched,
|
|
578
|
+
200,
|
|
579
|
+
);
|
|
580
|
+
// Write patched content to disk
|
|
581
|
+
if (projectRoot) {
|
|
582
|
+
const { writeFile } = await import("node:fs/promises");
|
|
583
|
+
const { join } = await import("node:path");
|
|
584
|
+
await writeFile(join(projectRoot, attrs.file), patched).catch(
|
|
585
|
+
() => {},
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
602
591
|
if (path.startsWith("rm://")) {
|
|
603
592
|
if (attrs?.path) {
|
|
604
593
|
await this.#knownStore.remove(runId, attrs.path);
|
|
594
|
+
if (projectRoot) {
|
|
595
|
+
const { unlink } = await import("node:fs/promises");
|
|
596
|
+
const { join } = await import("node:path");
|
|
597
|
+
await unlink(join(projectRoot, attrs.path)).catch(() => {});
|
|
598
|
+
}
|
|
605
599
|
}
|
|
606
600
|
}
|
|
607
601
|
|
|
@@ -617,68 +611,9 @@ export default class AgentLoop {
|
|
|
617
611
|
throw new Error(msg("error.resolution_invalid", { action }));
|
|
618
612
|
}
|
|
619
613
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
run: runAlias,
|
|
624
|
-
status: 202,
|
|
625
|
-
remainingCount: unresolved.length,
|
|
626
|
-
proposed: unresolved,
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Scope completion checks to the current loop
|
|
631
|
-
const currentLoop = await this.#db.get_current_loop.get({ run_id: runId });
|
|
632
|
-
const loopId = currentLoop?.id ?? null;
|
|
633
|
-
|
|
634
|
-
if (await this.#knownStore.hasRejections(runId, loopId)) {
|
|
635
|
-
if (currentLoop)
|
|
636
|
-
await this.#db.complete_loop.run({
|
|
637
|
-
id: loopId,
|
|
638
|
-
status: 200,
|
|
639
|
-
result: null,
|
|
640
|
-
});
|
|
641
|
-
await this.#db.update_run_status.run({ id: runId, status: 200 });
|
|
642
|
-
return { run: runAlias, status: 200 };
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const hasSummary = await this.#db.get_latest_summary.get({
|
|
646
|
-
run_id: runId,
|
|
647
|
-
loop_id: loopId,
|
|
648
|
-
});
|
|
649
|
-
if (hasSummary?.body) {
|
|
650
|
-
if (currentLoop)
|
|
651
|
-
await this.#db.complete_loop.run({
|
|
652
|
-
id: loopId,
|
|
653
|
-
status: 200,
|
|
654
|
-
result: null,
|
|
655
|
-
});
|
|
656
|
-
await this.#db.update_run_status.run({ id: runId, status: 200 });
|
|
657
|
-
return { run: runAlias, status: 200 };
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// No summary and no rejections in this loop — resume it
|
|
661
|
-
const projectId = runRow.project_id;
|
|
662
|
-
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
663
|
-
|
|
664
|
-
const latestPrompt = await this.#db.get_latest_prompt.get({
|
|
665
|
-
run_id: runId,
|
|
666
|
-
});
|
|
667
|
-
const resumeMode = latestPrompt?.attributes
|
|
668
|
-
? JSON.parse(latestPrompt.attributes).mode
|
|
669
|
-
: "ask";
|
|
670
|
-
|
|
671
|
-
// Re-enqueue the current loop's prompt to continue it
|
|
672
|
-
const loopSeq = await this.#db.next_loop.get({ run_id: runId });
|
|
673
|
-
await this.#db.enqueue_loop.get({
|
|
674
|
-
run_id: runId,
|
|
675
|
-
sequence: loopSeq.sequence,
|
|
676
|
-
mode: resumeMode,
|
|
677
|
-
model: runRow.model,
|
|
678
|
-
prompt: "",
|
|
679
|
-
config: "{}",
|
|
680
|
-
});
|
|
681
|
-
return this.#drainQueue(runId, runAlias, projectId, project, {});
|
|
614
|
+
// The dispatch loop is awaiting resolution. This unblocks it.
|
|
615
|
+
// Dispatch continuation is handled by the loop, not here.
|
|
616
|
+
return { run: runAlias, status: 200 };
|
|
682
617
|
}
|
|
683
618
|
|
|
684
619
|
async #composeResolvedContent(runId, path, _attrs, output) {
|
|
@@ -735,3 +670,13 @@ export default class AgentLoop {
|
|
|
735
670
|
return this.#knownStore.getLog(runRow.id);
|
|
736
671
|
}
|
|
737
672
|
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Pure recovery state transition — exported for testing.
|
|
676
|
+
*
|
|
677
|
+
* @param {object|null} recovery Current recovery state (mutated copy returned).
|
|
678
|
+
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
679
|
+
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
680
|
+
*/
|
|
681
|
+
// Re-export for backward compatibility with tests
|
|
682
|
+
export { advanceRecovery } from "../plugins/budget/recovery.js";
|
|
@@ -13,6 +13,7 @@ export default class ContextAssembler {
|
|
|
13
13
|
demoted = [],
|
|
14
14
|
toolSet = null,
|
|
15
15
|
lastContextTokens = 0,
|
|
16
|
+
turn = 1,
|
|
16
17
|
} = {},
|
|
17
18
|
hooks,
|
|
18
19
|
) {
|
|
@@ -30,6 +31,7 @@ export default class ContextAssembler {
|
|
|
30
31
|
lastContextTokens,
|
|
31
32
|
demoted,
|
|
32
33
|
toolSet,
|
|
34
|
+
turn,
|
|
33
35
|
};
|
|
34
36
|
|
|
35
37
|
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|