@possumtech/rummy 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -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 +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -330
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +229 -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 +275 -125
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- 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 +135 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -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 +260 -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 +122 -9
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +67 -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 +109 -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 +58 -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 +515 -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 +67 -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/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,138 @@ 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 = history
|
|
85
|
+
.filter((e) => {
|
|
86
|
+
// Updates are under the unified log namespace at
|
|
87
|
+
// log://turn_N/update/<slug>. Match by path pattern rather
|
|
88
|
+
// than scheme (scheme is now "log" for all log entries).
|
|
89
|
+
if (!/^log:\/\/turn_\d+\/update\//.test(e.path)) return false;
|
|
90
|
+
const attrs =
|
|
91
|
+
typeof e.attributes === "string"
|
|
92
|
+
? JSON.parse(e.attributes)
|
|
93
|
+
: e.attributes;
|
|
94
|
+
return attrs?.status === 200;
|
|
95
|
+
})
|
|
96
|
+
.at(-1);
|
|
97
|
+
|
|
98
|
+
// Always emit complete telemetry. When we don't have a fresh turn
|
|
99
|
+
// result (abort/max-turns/crash), read the last turn's context
|
|
100
|
+
// tokens from the DB instead. Both code paths compute a real
|
|
101
|
+
// budget from real data — never undefined, never invented.
|
|
102
|
+
const rows = await this.#db.get_turn_context.all({
|
|
103
|
+
run_id: runId,
|
|
104
|
+
turn,
|
|
105
|
+
});
|
|
106
|
+
let totalTokens;
|
|
107
|
+
if (result) {
|
|
108
|
+
totalTokens = result.assembledTokens;
|
|
109
|
+
} else {
|
|
110
|
+
// No fresh turn result — this happens on abort/max-turns/crash
|
|
111
|
+
// emits that fire before any turn executed, or after a turn
|
|
112
|
+
// that never produced tokens. Read the last turn's assembled
|
|
113
|
+
// context_tokens from the DB; absent means no turn ran yet
|
|
114
|
+
// (zero is the truth, not a fallback).
|
|
115
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
116
|
+
run_id: runId,
|
|
117
|
+
});
|
|
118
|
+
totalTokens = lastCtx ? lastCtx.context_tokens : 0;
|
|
119
|
+
}
|
|
120
|
+
const budget = computeBudget({ rows, contextSize, totalTokens });
|
|
121
|
+
|
|
122
|
+
await this.#hooks.run.state.emit({
|
|
123
|
+
projectId,
|
|
124
|
+
run: alias,
|
|
125
|
+
turn,
|
|
126
|
+
status,
|
|
127
|
+
summary: latestSummary?.body,
|
|
128
|
+
history,
|
|
129
|
+
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
130
|
+
telemetry: {
|
|
131
|
+
modelAlias: result?.modelAlias,
|
|
132
|
+
model: result?.model,
|
|
133
|
+
temperature: result?.temperature,
|
|
134
|
+
context_size: contextSize,
|
|
135
|
+
context_tokens: totalTokens,
|
|
136
|
+
ceiling: budget.ceiling,
|
|
137
|
+
token_usage: budget.tokenUsage,
|
|
138
|
+
tokens_free: budget.tokensFree,
|
|
139
|
+
prompt_tokens: runUsage.prompt_tokens,
|
|
140
|
+
cached_tokens: runUsage.cached_tokens,
|
|
141
|
+
completion_tokens: runUsage.completion_tokens,
|
|
142
|
+
reasoning_tokens: runUsage.reasoning_tokens,
|
|
143
|
+
total_tokens: runUsage.total_tokens,
|
|
144
|
+
cost: runUsage.cost,
|
|
145
|
+
context_distribution: await this.#db.get_turn_distribution.all({
|
|
146
|
+
run_id: runId,
|
|
147
|
+
turn,
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async #writeRunEntry(
|
|
154
|
+
runId,
|
|
155
|
+
alias,
|
|
156
|
+
prompt,
|
|
157
|
+
{
|
|
158
|
+
projectId,
|
|
159
|
+
parentRunId,
|
|
160
|
+
model,
|
|
161
|
+
persona = null,
|
|
162
|
+
temperature = null,
|
|
163
|
+
contextLimit = null,
|
|
164
|
+
},
|
|
165
|
+
) {
|
|
166
|
+
await this.#entries.set({
|
|
167
|
+
runId,
|
|
168
|
+
turn: 0,
|
|
169
|
+
path: `run://${alias}`,
|
|
170
|
+
body: prompt ? prompt : "",
|
|
171
|
+
state: "proposed",
|
|
172
|
+
attributes: {
|
|
173
|
+
projectId,
|
|
174
|
+
parentRunId,
|
|
175
|
+
model,
|
|
176
|
+
persona,
|
|
177
|
+
temperature,
|
|
178
|
+
contextLimit,
|
|
179
|
+
},
|
|
180
|
+
writer: "system",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async ensureRun(projectId, model, run, prompt, options = {}) {
|
|
185
|
+
const {
|
|
186
|
+
fork: isFork = false,
|
|
187
|
+
temperature = null,
|
|
188
|
+
persona = null,
|
|
189
|
+
contextLimit = null,
|
|
190
|
+
} = options;
|
|
37
191
|
const requestedModel = model;
|
|
38
192
|
|
|
39
193
|
if (run && isFork) {
|
|
@@ -46,13 +200,18 @@ export default class AgentLoop {
|
|
|
46
200
|
parent_run_id: existingRun.id,
|
|
47
201
|
model: requestedModel,
|
|
48
202
|
alias,
|
|
49
|
-
temperature
|
|
50
|
-
persona
|
|
51
|
-
context_limit:
|
|
203
|
+
temperature,
|
|
204
|
+
persona,
|
|
205
|
+
context_limit: contextLimit,
|
|
52
206
|
});
|
|
53
|
-
await this.#
|
|
54
|
-
|
|
55
|
-
|
|
207
|
+
await this.#entries.forkEntries(existingRun.id, runRow.id);
|
|
208
|
+
await this.#writeRunEntry(runRow.id, alias, prompt, {
|
|
209
|
+
projectId,
|
|
210
|
+
parentRunId: existingRun.id,
|
|
211
|
+
model: requestedModel,
|
|
212
|
+
persona,
|
|
213
|
+
temperature,
|
|
214
|
+
contextLimit,
|
|
56
215
|
});
|
|
57
216
|
await this.#hooks.run.created.emit({
|
|
58
217
|
runId: runRow.id,
|
|
@@ -64,34 +223,43 @@ export default class AgentLoop {
|
|
|
64
223
|
|
|
65
224
|
if (run) {
|
|
66
225
|
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
|
-
|
|
226
|
+
if (existingRun) {
|
|
227
|
+
const existing = this.#activeRuns.get(existingRun.id);
|
|
228
|
+
if (existing) existing.controller.abort();
|
|
229
|
+
|
|
230
|
+
// Clean up stale proposals from interrupted runs
|
|
231
|
+
const unresolved = await this.#entries.getUnresolved(existingRun.id);
|
|
232
|
+
for (const u of unresolved) {
|
|
233
|
+
await this.#entries.set({
|
|
234
|
+
runId: existingRun.id,
|
|
235
|
+
path: u.path,
|
|
236
|
+
state: "cancelled",
|
|
237
|
+
body: "Stale proposal from interrupted run",
|
|
238
|
+
outcome: "interrupted",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return { runId: existingRun.id, alias: existingRun.alias };
|
|
82
242
|
}
|
|
83
|
-
|
|
243
|
+
// Client-specified alias for a brand-new run — accept it verbatim.
|
|
84
244
|
}
|
|
85
245
|
|
|
86
|
-
const alias = await this.#generateAlias(requestedModel);
|
|
246
|
+
const alias = run ? run : await this.#generateAlias(requestedModel);
|
|
87
247
|
const runRow = await this.#db.create_run.get({
|
|
88
248
|
project_id: projectId,
|
|
89
249
|
parent_run_id: null,
|
|
90
250
|
model: requestedModel,
|
|
91
251
|
alias,
|
|
92
|
-
temperature
|
|
93
|
-
persona
|
|
94
|
-
context_limit:
|
|
252
|
+
temperature,
|
|
253
|
+
persona,
|
|
254
|
+
context_limit: contextLimit,
|
|
255
|
+
});
|
|
256
|
+
await this.#writeRunEntry(runRow.id, alias, prompt, {
|
|
257
|
+
projectId,
|
|
258
|
+
parentRunId: null,
|
|
259
|
+
model: requestedModel,
|
|
260
|
+
persona,
|
|
261
|
+
temperature,
|
|
262
|
+
contextLimit,
|
|
95
263
|
});
|
|
96
264
|
await this.#hooks.run.created.emit({ runId: runRow.id, alias });
|
|
97
265
|
return { runId: runRow.id, alias };
|
|
@@ -125,7 +293,13 @@ export default class AgentLoop {
|
|
|
125
293
|
const noProposals = options?.noProposals === true;
|
|
126
294
|
const requestedModel = model;
|
|
127
295
|
|
|
128
|
-
const runInfo = await this
|
|
296
|
+
const runInfo = await this.ensureRun(
|
|
297
|
+
projectId,
|
|
298
|
+
model,
|
|
299
|
+
run,
|
|
300
|
+
prompt,
|
|
301
|
+
options,
|
|
302
|
+
);
|
|
129
303
|
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
130
304
|
|
|
131
305
|
const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
@@ -134,7 +308,7 @@ export default class AgentLoop {
|
|
|
134
308
|
sequence: loopSeq.sequence,
|
|
135
309
|
mode,
|
|
136
310
|
model: requestedModel,
|
|
137
|
-
prompt: prompt
|
|
311
|
+
prompt: prompt ? prompt : "",
|
|
138
312
|
config: JSON.stringify({
|
|
139
313
|
noRepo,
|
|
140
314
|
noInteraction,
|
|
@@ -148,28 +322,52 @@ export default class AgentLoop {
|
|
|
148
322
|
return { run: currentAlias, status: 100 };
|
|
149
323
|
}
|
|
150
324
|
|
|
151
|
-
|
|
325
|
+
// Allocate the controller + Promise pair here so `abortAll` can
|
|
326
|
+
// reach both — abort the controller, await the Promise's drain.
|
|
327
|
+
const controller = new AbortController();
|
|
328
|
+
const promise = this.#drainQueue(
|
|
152
329
|
currentRunId,
|
|
153
330
|
currentAlias,
|
|
154
331
|
projectId,
|
|
155
332
|
project,
|
|
156
333
|
options,
|
|
334
|
+
controller,
|
|
157
335
|
);
|
|
336
|
+
this.#activeRuns.set(currentRunId, { controller, promise });
|
|
337
|
+
return promise;
|
|
158
338
|
}
|
|
159
339
|
|
|
160
|
-
async #drainQueue(
|
|
161
|
-
|
|
162
|
-
|
|
340
|
+
async #drainQueue(
|
|
341
|
+
currentRunId,
|
|
342
|
+
currentAlias,
|
|
343
|
+
projectId,
|
|
344
|
+
project,
|
|
345
|
+
options,
|
|
346
|
+
controller,
|
|
347
|
+
) {
|
|
348
|
+
console.error(`[DRAIN] ${currentAlias} enter (runId=${currentRunId})`);
|
|
163
349
|
|
|
164
350
|
try {
|
|
165
351
|
while (true) {
|
|
166
352
|
const loop = await this.#db.claim_next_loop.get({
|
|
167
353
|
run_id: currentRunId,
|
|
168
354
|
});
|
|
169
|
-
if (!loop)
|
|
355
|
+
if (!loop) {
|
|
356
|
+
console.error(`[DRAIN] ${currentAlias} queue empty — exiting`);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
console.error(
|
|
360
|
+
`[DRAIN] ${currentAlias} claimed loop id=${loop.id} mode=${loop.mode} seq=${loop.sequence}`,
|
|
361
|
+
);
|
|
170
362
|
|
|
171
|
-
const loopConfig =
|
|
363
|
+
const loopConfig = JSON.parse(loop.config);
|
|
172
364
|
const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
365
|
+
const {
|
|
366
|
+
noRepo = false,
|
|
367
|
+
noInteraction = false,
|
|
368
|
+
noWeb = false,
|
|
369
|
+
noProposals = false,
|
|
370
|
+
} = loopConfig;
|
|
173
371
|
|
|
174
372
|
let result;
|
|
175
373
|
try {
|
|
@@ -182,15 +380,18 @@ export default class AgentLoop {
|
|
|
182
380
|
currentLoopId: loop.id,
|
|
183
381
|
requestedModel: loop.model,
|
|
184
382
|
prompt: loop.prompt,
|
|
185
|
-
noRepo
|
|
186
|
-
noInteraction
|
|
187
|
-
noWeb
|
|
188
|
-
noProposals
|
|
383
|
+
noRepo,
|
|
384
|
+
noInteraction,
|
|
385
|
+
noWeb,
|
|
386
|
+
noProposals,
|
|
189
387
|
options: { ...options, temperature: loopConfig.temperature },
|
|
190
388
|
hook,
|
|
191
389
|
signal: controller.signal,
|
|
192
390
|
});
|
|
193
391
|
} catch (err) {
|
|
392
|
+
console.error(
|
|
393
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} threw: ${err.message}`,
|
|
394
|
+
);
|
|
194
395
|
await this.#db.complete_loop.run({
|
|
195
396
|
id: loop.id,
|
|
196
397
|
status: 500,
|
|
@@ -200,6 +401,9 @@ export default class AgentLoop {
|
|
|
200
401
|
}
|
|
201
402
|
|
|
202
403
|
if (result.status === 413) {
|
|
404
|
+
console.error(
|
|
405
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} overflow=413`,
|
|
406
|
+
);
|
|
203
407
|
await this.#db.complete_loop.run({
|
|
204
408
|
id: loop.id,
|
|
205
409
|
status: 413,
|
|
@@ -212,6 +416,9 @@ export default class AgentLoop {
|
|
|
212
416
|
};
|
|
213
417
|
}
|
|
214
418
|
|
|
419
|
+
console.error(
|
|
420
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} completed status=${result.status}`,
|
|
421
|
+
);
|
|
215
422
|
await this.#db.complete_loop.run({
|
|
216
423
|
id: loop.id,
|
|
217
424
|
status: result.status,
|
|
@@ -222,7 +429,7 @@ export default class AgentLoop {
|
|
|
222
429
|
const runRow = await this.#db.get_run_by_alias.get({
|
|
223
430
|
alias: currentAlias,
|
|
224
431
|
});
|
|
225
|
-
return { run: currentAlias, status: runRow
|
|
432
|
+
return { run: currentAlias, status: runRow.status };
|
|
226
433
|
} finally {
|
|
227
434
|
this.#activeRuns.delete(currentRunId);
|
|
228
435
|
}
|
|
@@ -247,10 +454,7 @@ export default class AgentLoop {
|
|
|
247
454
|
}) {
|
|
248
455
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
249
456
|
if (runRow.status !== 102) {
|
|
250
|
-
await this.#
|
|
251
|
-
id: currentRunId,
|
|
252
|
-
status: 102,
|
|
253
|
-
});
|
|
457
|
+
await this.#setRunStatus(currentRunId, currentAlias, 102);
|
|
254
458
|
}
|
|
255
459
|
|
|
256
460
|
const modelContextSize =
|
|
@@ -266,20 +470,7 @@ export default class AgentLoop {
|
|
|
266
470
|
});
|
|
267
471
|
|
|
268
472
|
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);
|
|
473
|
+
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS);
|
|
283
474
|
|
|
284
475
|
await this.#hooks.loop.started.emit({
|
|
285
476
|
runId: currentRunId,
|
|
@@ -291,9 +482,17 @@ export default class AgentLoop {
|
|
|
291
482
|
try {
|
|
292
483
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
293
484
|
if (signal.aborted) {
|
|
294
|
-
|
|
295
|
-
|
|
485
|
+
console.error(
|
|
486
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} ABORT via signal`,
|
|
487
|
+
);
|
|
488
|
+
await this.#setRunStatus(currentRunId, currentAlias, 499);
|
|
489
|
+
await this.#emitRunState({
|
|
490
|
+
projectId,
|
|
491
|
+
runId: currentRunId,
|
|
492
|
+
alias: currentAlias,
|
|
493
|
+
turn: loopIteration,
|
|
296
494
|
status: 499,
|
|
495
|
+
contextSize,
|
|
297
496
|
});
|
|
298
497
|
const out = {
|
|
299
498
|
run: currentAlias,
|
|
@@ -304,6 +503,9 @@ export default class AgentLoop {
|
|
|
304
503
|
return out;
|
|
305
504
|
}
|
|
306
505
|
loopIteration++;
|
|
506
|
+
console.error(
|
|
507
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} ENTER (max=${MAX_LOOP_ITERATIONS})`,
|
|
508
|
+
);
|
|
307
509
|
|
|
308
510
|
let turnPrompt;
|
|
309
511
|
if (loopIteration === 1) {
|
|
@@ -315,6 +517,9 @@ export default class AgentLoop {
|
|
|
315
517
|
);
|
|
316
518
|
}
|
|
317
519
|
|
|
520
|
+
console.error(
|
|
521
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} executing turn`,
|
|
522
|
+
);
|
|
318
523
|
const result = await this.#turnExecutor.execute({
|
|
319
524
|
mode,
|
|
320
525
|
project,
|
|
@@ -326,198 +531,122 @@ export default class AgentLoop {
|
|
|
326
531
|
loopPrompt: turnPrompt,
|
|
327
532
|
loopIteration,
|
|
328
533
|
noRepo,
|
|
534
|
+
noWeb,
|
|
535
|
+
noInteraction,
|
|
536
|
+
noProposals,
|
|
329
537
|
toolSet,
|
|
330
|
-
inRecovery: recovery !== null,
|
|
331
538
|
contextSize,
|
|
332
539
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
333
540
|
signal,
|
|
334
541
|
});
|
|
542
|
+
console.error(
|
|
543
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} turn done: status=${result.status} turn=${result.turn}`,
|
|
544
|
+
);
|
|
335
545
|
|
|
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,
|
|
546
|
+
const verdict = await this.#hooks.error.verdict({
|
|
547
|
+
store: this.#entries,
|
|
548
|
+
runId: currentRunId,
|
|
549
|
+
loopId: currentLoopId,
|
|
550
|
+
turn: result.turn,
|
|
551
|
+
recorded: result.recorded,
|
|
552
|
+
summaryText: result.summaryText,
|
|
390
553
|
});
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
554
|
+
const vStatus = verdict.status === undefined ? "-" : verdict.status;
|
|
555
|
+
const vReason = verdict.reason ? verdict.reason : "-";
|
|
556
|
+
console.error(
|
|
557
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} verdict: continue=${verdict.continue} status=${vStatus} reason=${vReason}`,
|
|
558
|
+
);
|
|
394
559
|
|
|
395
|
-
await this.#
|
|
560
|
+
await this.#emitRunState({
|
|
396
561
|
projectId,
|
|
397
|
-
|
|
562
|
+
runId: currentRunId,
|
|
563
|
+
alias: currentAlias,
|
|
398
564
|
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
|
-
},
|
|
565
|
+
status: verdict.continue ? 102 : verdict.status,
|
|
566
|
+
contextSize,
|
|
567
|
+
result,
|
|
426
568
|
});
|
|
427
569
|
await this.#hooks.run.step.completed.emit({
|
|
428
570
|
projectId,
|
|
429
571
|
run: currentAlias,
|
|
430
572
|
turn: result.turn,
|
|
431
|
-
flags: result.flags,
|
|
432
573
|
});
|
|
574
|
+
if (verdict.continue) continue;
|
|
433
575
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
await this.#
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
});
|
|
443
|
-
const out = {
|
|
444
|
-
run: currentAlias,
|
|
445
|
-
status: 200,
|
|
576
|
+
console.error(
|
|
577
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} CLOSE status=${verdict.status} reason=${vReason}`,
|
|
578
|
+
);
|
|
579
|
+
await this.#setRunStatus(currentRunId, currentAlias, verdict.status);
|
|
580
|
+
if (verdict.reason) {
|
|
581
|
+
await this.#hooks.error.log.emit({
|
|
582
|
+
store: this.#entries,
|
|
583
|
+
runId: currentRunId,
|
|
446
584
|
turn: result.turn,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return out;
|
|
585
|
+
loopId: currentLoopId,
|
|
586
|
+
message: verdict.reason,
|
|
587
|
+
});
|
|
451
588
|
}
|
|
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
589
|
const out = {
|
|
461
590
|
run: currentAlias,
|
|
462
|
-
status:
|
|
591
|
+
status: verdict.status,
|
|
463
592
|
turn: result.turn,
|
|
593
|
+
reason: verdict.reason,
|
|
464
594
|
};
|
|
465
595
|
await hook.completed.emit({ projectId, ...out });
|
|
466
596
|
return out;
|
|
467
597
|
}
|
|
468
598
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
599
|
+
// MAX_TURNS exhaustion without a terminal update is abandonment.
|
|
600
|
+
console.error(
|
|
601
|
+
`[LOOP] ${currentAlias} hit MAX_LOOP_ITERATIONS=${MAX_LOOP_ITERATIONS} — abandoning at 499`,
|
|
602
|
+
);
|
|
603
|
+
await this.#setRunStatus(currentRunId, currentAlias, 499);
|
|
604
|
+
await this.#emitRunState({
|
|
605
|
+
projectId,
|
|
606
|
+
runId: currentRunId,
|
|
607
|
+
alias: currentAlias,
|
|
608
|
+
turn: loopIteration,
|
|
609
|
+
status: 499,
|
|
610
|
+
contextSize,
|
|
472
611
|
});
|
|
473
612
|
const out = {
|
|
474
613
|
run: currentAlias,
|
|
475
|
-
status:
|
|
614
|
+
status: 499,
|
|
476
615
|
turn: loopIteration,
|
|
477
616
|
};
|
|
478
617
|
await hook.completed.emit({ projectId, ...out });
|
|
479
618
|
return out;
|
|
480
619
|
} catch (err) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
620
|
+
const status = signal.aborted ? 499 : 500;
|
|
621
|
+
await this.#setRunStatus(currentRunId, currentAlias, status);
|
|
622
|
+
await this.#emitRunState({
|
|
623
|
+
projectId,
|
|
624
|
+
runId: currentRunId,
|
|
625
|
+
alias: currentAlias,
|
|
626
|
+
turn: loopIteration,
|
|
627
|
+
status,
|
|
628
|
+
contextSize,
|
|
629
|
+
});
|
|
630
|
+
if (status === 500) {
|
|
631
|
+
await this.#hooks.error.log.emit({
|
|
632
|
+
store: this.#entries,
|
|
633
|
+
runId: currentRunId,
|
|
634
|
+
turn: loopIteration,
|
|
635
|
+
loopId: currentLoopId,
|
|
636
|
+
message: `${err.message}\n${err.stack}`,
|
|
485
637
|
});
|
|
486
|
-
return { run: currentAlias, status: 499, turn: loopIteration };
|
|
487
638
|
}
|
|
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
|
-
};
|
|
639
|
+
const out = { run: currentAlias, status, turn: loopIteration };
|
|
640
|
+
if (status === 500) out.error = err.message;
|
|
510
641
|
await hook.completed.emit({ projectId, ...out });
|
|
511
642
|
return out;
|
|
512
643
|
} finally {
|
|
513
|
-
await this.#hooks.loop.completed
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
})
|
|
520
|
-
.catch(() => {});
|
|
644
|
+
await this.#hooks.loop.completed.emit({
|
|
645
|
+
runId: currentRunId,
|
|
646
|
+
loopId: currentLoopId,
|
|
647
|
+
mode,
|
|
648
|
+
turns: loopIteration,
|
|
649
|
+
});
|
|
521
650
|
}
|
|
522
651
|
}
|
|
523
652
|
|
|
@@ -529,119 +658,120 @@ export default class AgentLoop {
|
|
|
529
658
|
|
|
530
659
|
const { path, action, output } = resolution;
|
|
531
660
|
|
|
532
|
-
if (action
|
|
533
|
-
|
|
534
|
-
|
|
661
|
+
if (action !== "accept" && action !== "error" && action !== "reject") {
|
|
662
|
+
throw new Error(msg("error.resolution_invalid", { action }));
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (action === "reject") {
|
|
666
|
+
await this.#entries.set({
|
|
667
|
+
runId,
|
|
668
|
+
path,
|
|
669
|
+
state: "failed",
|
|
670
|
+
body: output ? output : "rejected",
|
|
671
|
+
outcome: "permission",
|
|
672
|
+
});
|
|
673
|
+
await this.#hooks.proposal.rejected.emit({
|
|
535
674
|
runId,
|
|
675
|
+
runRow,
|
|
536
676
|
path,
|
|
537
|
-
attrs,
|
|
538
677
|
output,
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
}
|
|
678
|
+
db: this.#db,
|
|
679
|
+
entries: this.#entries,
|
|
680
|
+
});
|
|
681
|
+
// Report the CURRENT run status (typically 102 mid-run) so the
|
|
682
|
+
// client's dispatch handler doesn't mistake a successful
|
|
683
|
+
// resolve's HTTP-style 200 ack for a terminal run status and
|
|
684
|
+
// prematurely close the document. Real terminal state comes
|
|
685
|
+
// from the run/state notification at end-of-turn.
|
|
686
|
+
return { run: runAlias, status: runRow.status };
|
|
687
|
+
}
|
|
551
688
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
689
|
+
const attrs = await this.#entries.getAttributes(runId, path);
|
|
690
|
+
const project = await this.#db.get_project_by_id.get({
|
|
691
|
+
id: runRow.project_id,
|
|
692
|
+
});
|
|
693
|
+
const ctx = {
|
|
694
|
+
runId,
|
|
695
|
+
runRow,
|
|
696
|
+
projectId: runRow.project_id,
|
|
697
|
+
projectRoot: project?.project_root,
|
|
698
|
+
path,
|
|
699
|
+
attrs,
|
|
700
|
+
output,
|
|
701
|
+
db: this.#db,
|
|
702
|
+
entries: this.#entries,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// Plugins veto acceptance (e.g. readonly) via proposal.accepting.
|
|
706
|
+
// First veto wins: state=failed with plugin-supplied outcome + body.
|
|
707
|
+
if (action === "accept") {
|
|
708
|
+
const veto = await this.#hooks.proposal.accepting.filter(null, ctx);
|
|
709
|
+
if (veto?.allow === false) {
|
|
710
|
+
await this.#entries.set({
|
|
711
|
+
runId,
|
|
712
|
+
path,
|
|
713
|
+
state: "failed",
|
|
714
|
+
outcome: veto.outcome,
|
|
715
|
+
body: veto.body,
|
|
556
716
|
});
|
|
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
|
-
}
|
|
717
|
+
return { ok: true, state: "failed", outcome: veto.outcome };
|
|
606
718
|
}
|
|
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
719
|
}
|
|
612
720
|
|
|
613
|
-
//
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
721
|
+
// Compose the resolved body. Default is output || "". Plugins may
|
|
722
|
+
// override via proposal.content (e.g. set prefers the existing
|
|
723
|
+
// proposed body from the log entry).
|
|
724
|
+
const defaultBody = output ? output : "";
|
|
725
|
+
const resolvedBody = await this.#hooks.proposal.content.filter(
|
|
726
|
+
defaultBody,
|
|
727
|
+
ctx,
|
|
728
|
+
);
|
|
729
|
+
const state = action === "error" ? "failed" : "resolved";
|
|
730
|
+
const outcome = action === "error" ? "error" : null;
|
|
731
|
+
const existing = await this.#entries.getState(runId, path);
|
|
732
|
+
const existingTurn = existing?.turn === undefined ? 0 : existing.turn;
|
|
733
|
+
await this.#entries.set({
|
|
734
|
+
runId,
|
|
735
|
+
turn: existingTurn,
|
|
736
|
+
path,
|
|
737
|
+
state,
|
|
738
|
+
body: resolvedBody,
|
|
739
|
+
outcome,
|
|
740
|
+
});
|
|
617
741
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
742
|
+
const event =
|
|
743
|
+
action === "accept"
|
|
744
|
+
? this.#hooks.proposal.accepted
|
|
745
|
+
: this.#hooks.proposal.rejected;
|
|
746
|
+
await event.emit({ ...ctx, resolvedBody });
|
|
747
|
+
|
|
748
|
+
// Same rationale as the reject path: return current run status
|
|
749
|
+
// (102 mid-run) rather than a hardcoded 200 so the nvim client
|
|
750
|
+
// doesn't treat the RPC ack as a terminal signal.
|
|
751
|
+
return { run: runAlias, status: runRow.status };
|
|
628
752
|
}
|
|
629
753
|
|
|
630
|
-
async inject(runAlias, message) {
|
|
754
|
+
async inject(runAlias, message, mode) {
|
|
755
|
+
if (mode !== "ask" && mode !== "act") {
|
|
756
|
+
throw new Error(
|
|
757
|
+
`inject: mode is required and must be "ask" or "act" (got ${JSON.stringify(mode)})`,
|
|
758
|
+
);
|
|
759
|
+
}
|
|
631
760
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
632
761
|
if (!runRow)
|
|
633
762
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
634
763
|
|
|
635
764
|
const nextTurn = runRow.next_turn;
|
|
636
765
|
|
|
637
|
-
await this.#
|
|
638
|
-
runRow.id,
|
|
639
|
-
nextTurn,
|
|
640
|
-
`prompt://${nextTurn}`,
|
|
641
|
-
message,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
766
|
+
await this.#entries.set({
|
|
767
|
+
runId: runRow.id,
|
|
768
|
+
turn: nextTurn,
|
|
769
|
+
path: `prompt://${nextTurn}`,
|
|
770
|
+
body: message,
|
|
771
|
+
state: "resolved",
|
|
772
|
+
attributes: { mode },
|
|
773
|
+
writer: "plugin",
|
|
774
|
+
});
|
|
645
775
|
|
|
646
776
|
if (this.#activeRuns.has(runRow.id)) {
|
|
647
777
|
return { run: runAlias, status: runRow.status, injected: "next_turn" };
|
|
@@ -651,7 +781,7 @@ export default class AgentLoop {
|
|
|
651
781
|
await this.#db.enqueue_loop.get({
|
|
652
782
|
run_id: runRow.id,
|
|
653
783
|
sequence: injectLoopSeq.sequence,
|
|
654
|
-
mode
|
|
784
|
+
mode,
|
|
655
785
|
model: runRow.model,
|
|
656
786
|
prompt: message,
|
|
657
787
|
config: "{}",
|
|
@@ -659,23 +789,23 @@ export default class AgentLoop {
|
|
|
659
789
|
|
|
660
790
|
const projectId = runRow.project_id;
|
|
661
791
|
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
662
|
-
|
|
792
|
+
const controller = new AbortController();
|
|
793
|
+
const promise = this.#drainQueue(
|
|
794
|
+
runRow.id,
|
|
795
|
+
runAlias,
|
|
796
|
+
projectId,
|
|
797
|
+
project,
|
|
798
|
+
{},
|
|
799
|
+
controller,
|
|
800
|
+
);
|
|
801
|
+
this.#activeRuns.set(runRow.id, { controller, promise });
|
|
802
|
+
return promise;
|
|
663
803
|
}
|
|
664
804
|
|
|
665
805
|
async getRunHistory(runAlias) {
|
|
666
806
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
667
807
|
if (!runRow)
|
|
668
808
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
669
|
-
return this.#
|
|
809
|
+
return this.#entries.getLog(runRow.id);
|
|
670
810
|
}
|
|
671
811
|
}
|
|
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";
|