@possumtech/rummy 0.4.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 -4
- 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 -331
- package/src/agent/ContextAssembler.js +4 -2
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -379
- package/src/agent/XmlParser.js +242 -67
- 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 -118
- 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 +35 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +140 -37
- package/src/hooks/ToolRegistry.js +36 -35
- 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 -23
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -23
- package/src/plugins/budget/budget.js +261 -69
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +31 -13
- package/src/plugins/cp/cpDoc.js +2 -23
- 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 +47 -8
- 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 -7
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +125 -49
- package/src/plugins/get/getDoc.js +2 -43
- 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 +43 -3
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +126 -12
- 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 +77 -45
- package/src/plugins/known/knownDoc.js +2 -29
- 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 +57 -24
- package/src/plugins/mv/mvDoc.js +2 -29
- 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 +63 -18
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +58 -14
- package/src/plugins/rm/rmDoc.js +2 -24
- 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 -77
- 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 +52 -8
- 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 -17
- 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 +148 -74
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +14 -1
- package/src/plugins/think/thinkDoc.js +2 -17
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +56 -21
- package/src/plugins/unknown/unknownDoc.js +2 -25
- 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 -27
- 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/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/src/agent/KnownStore.js +0 -338
- package/src/agent/ResponseHealer.js +0 -188
- 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 -37
- 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 -60
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -26
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -28
package/src/agent/AgentLoop.js
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import KnownStore from "./KnownStore.js";
|
|
1
|
+
import { computeBudget } from "./budget.js";
|
|
3
2
|
import msg from "./messages.js";
|
|
4
|
-
|
|
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
|
+
};
|
|
5
12
|
|
|
6
13
|
export default class AgentLoop {
|
|
7
14
|
#db;
|
|
8
15
|
#llmProvider;
|
|
9
16
|
#hooks;
|
|
10
17
|
#turnExecutor;
|
|
11
|
-
#
|
|
18
|
+
#entries;
|
|
12
19
|
#activeRuns = new Map();
|
|
13
20
|
|
|
14
|
-
constructor(db, llmProvider, hooks, turnExecutor,
|
|
21
|
+
constructor(db, llmProvider, hooks, turnExecutor, entries) {
|
|
15
22
|
this.#db = db;
|
|
16
23
|
this.#llmProvider = llmProvider;
|
|
17
24
|
this.#hooks = hooks;
|
|
18
25
|
this.#turnExecutor = turnExecutor;
|
|
19
|
-
this.#
|
|
26
|
+
this.#entries = entries;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
abort(runId) {
|
|
23
|
-
const
|
|
24
|
-
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);
|
|
25
49
|
}
|
|
26
50
|
|
|
27
51
|
async #generateAlias(modelAlias) {
|
|
@@ -32,9 +56,138 @@ export default class AgentLoop {
|
|
|
32
56
|
return `Turn ${turn}/${maxTurns}`;
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
async #
|
|
36
|
-
|
|
37
|
-
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;
|
|
38
191
|
const requestedModel = model;
|
|
39
192
|
|
|
40
193
|
if (run && isFork) {
|
|
@@ -47,13 +200,18 @@ export default class AgentLoop {
|
|
|
47
200
|
parent_run_id: existingRun.id,
|
|
48
201
|
model: requestedModel,
|
|
49
202
|
alias,
|
|
50
|
-
temperature
|
|
51
|
-
persona
|
|
52
|
-
context_limit:
|
|
203
|
+
temperature,
|
|
204
|
+
persona,
|
|
205
|
+
context_limit: contextLimit,
|
|
53
206
|
});
|
|
54
|
-
await this.#
|
|
55
|
-
|
|
56
|
-
|
|
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,
|
|
57
215
|
});
|
|
58
216
|
await this.#hooks.run.created.emit({
|
|
59
217
|
runId: runRow.id,
|
|
@@ -65,34 +223,43 @@ export default class AgentLoop {
|
|
|
65
223
|
|
|
66
224
|
if (run) {
|
|
67
225
|
const existingRun = await this.#db.get_run_by_alias.get({ alias: run });
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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 };
|
|
83
242
|
}
|
|
84
|
-
|
|
243
|
+
// Client-specified alias for a brand-new run — accept it verbatim.
|
|
85
244
|
}
|
|
86
245
|
|
|
87
|
-
const alias = await this.#generateAlias(requestedModel);
|
|
246
|
+
const alias = run ? run : await this.#generateAlias(requestedModel);
|
|
88
247
|
const runRow = await this.#db.create_run.get({
|
|
89
248
|
project_id: projectId,
|
|
90
249
|
parent_run_id: null,
|
|
91
250
|
model: requestedModel,
|
|
92
251
|
alias,
|
|
93
|
-
temperature
|
|
94
|
-
persona
|
|
95
|
-
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,
|
|
96
263
|
});
|
|
97
264
|
await this.#hooks.run.created.emit({ runId: runRow.id, alias });
|
|
98
265
|
return { runId: runRow.id, alias };
|
|
@@ -126,7 +293,13 @@ export default class AgentLoop {
|
|
|
126
293
|
const noProposals = options?.noProposals === true;
|
|
127
294
|
const requestedModel = model;
|
|
128
295
|
|
|
129
|
-
const runInfo = await this
|
|
296
|
+
const runInfo = await this.ensureRun(
|
|
297
|
+
projectId,
|
|
298
|
+
model,
|
|
299
|
+
run,
|
|
300
|
+
prompt,
|
|
301
|
+
options,
|
|
302
|
+
);
|
|
130
303
|
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
131
304
|
|
|
132
305
|
const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
@@ -135,7 +308,7 @@ export default class AgentLoop {
|
|
|
135
308
|
sequence: loopSeq.sequence,
|
|
136
309
|
mode,
|
|
137
310
|
model: requestedModel,
|
|
138
|
-
prompt: prompt
|
|
311
|
+
prompt: prompt ? prompt : "",
|
|
139
312
|
config: JSON.stringify({
|
|
140
313
|
noRepo,
|
|
141
314
|
noInteraction,
|
|
@@ -149,28 +322,52 @@ export default class AgentLoop {
|
|
|
149
322
|
return { run: currentAlias, status: 100 };
|
|
150
323
|
}
|
|
151
324
|
|
|
152
|
-
|
|
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(
|
|
153
329
|
currentRunId,
|
|
154
330
|
currentAlias,
|
|
155
331
|
projectId,
|
|
156
332
|
project,
|
|
157
333
|
options,
|
|
334
|
+
controller,
|
|
158
335
|
);
|
|
336
|
+
this.#activeRuns.set(currentRunId, { controller, promise });
|
|
337
|
+
return promise;
|
|
159
338
|
}
|
|
160
339
|
|
|
161
|
-
async #drainQueue(
|
|
162
|
-
|
|
163
|
-
|
|
340
|
+
async #drainQueue(
|
|
341
|
+
currentRunId,
|
|
342
|
+
currentAlias,
|
|
343
|
+
projectId,
|
|
344
|
+
project,
|
|
345
|
+
options,
|
|
346
|
+
controller,
|
|
347
|
+
) {
|
|
348
|
+
console.error(`[DRAIN] ${currentAlias} enter (runId=${currentRunId})`);
|
|
164
349
|
|
|
165
350
|
try {
|
|
166
351
|
while (true) {
|
|
167
352
|
const loop = await this.#db.claim_next_loop.get({
|
|
168
353
|
run_id: currentRunId,
|
|
169
354
|
});
|
|
170
|
-
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
|
+
);
|
|
171
362
|
|
|
172
|
-
const loopConfig =
|
|
363
|
+
const loopConfig = JSON.parse(loop.config);
|
|
173
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;
|
|
174
371
|
|
|
175
372
|
let result;
|
|
176
373
|
try {
|
|
@@ -183,15 +380,18 @@ export default class AgentLoop {
|
|
|
183
380
|
currentLoopId: loop.id,
|
|
184
381
|
requestedModel: loop.model,
|
|
185
382
|
prompt: loop.prompt,
|
|
186
|
-
noRepo
|
|
187
|
-
noInteraction
|
|
188
|
-
noWeb
|
|
189
|
-
noProposals
|
|
383
|
+
noRepo,
|
|
384
|
+
noInteraction,
|
|
385
|
+
noWeb,
|
|
386
|
+
noProposals,
|
|
190
387
|
options: { ...options, temperature: loopConfig.temperature },
|
|
191
388
|
hook,
|
|
192
389
|
signal: controller.signal,
|
|
193
390
|
});
|
|
194
391
|
} catch (err) {
|
|
392
|
+
console.error(
|
|
393
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} threw: ${err.message}`,
|
|
394
|
+
);
|
|
195
395
|
await this.#db.complete_loop.run({
|
|
196
396
|
id: loop.id,
|
|
197
397
|
status: 500,
|
|
@@ -201,6 +401,9 @@ export default class AgentLoop {
|
|
|
201
401
|
}
|
|
202
402
|
|
|
203
403
|
if (result.status === 413) {
|
|
404
|
+
console.error(
|
|
405
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} overflow=413`,
|
|
406
|
+
);
|
|
204
407
|
await this.#db.complete_loop.run({
|
|
205
408
|
id: loop.id,
|
|
206
409
|
status: 413,
|
|
@@ -213,6 +416,9 @@ export default class AgentLoop {
|
|
|
213
416
|
};
|
|
214
417
|
}
|
|
215
418
|
|
|
419
|
+
console.error(
|
|
420
|
+
`[DRAIN] ${currentAlias} loop id=${loop.id} completed status=${result.status}`,
|
|
421
|
+
);
|
|
216
422
|
await this.#db.complete_loop.run({
|
|
217
423
|
id: loop.id,
|
|
218
424
|
status: result.status,
|
|
@@ -223,7 +429,7 @@ export default class AgentLoop {
|
|
|
223
429
|
const runRow = await this.#db.get_run_by_alias.get({
|
|
224
430
|
alias: currentAlias,
|
|
225
431
|
});
|
|
226
|
-
return { run: currentAlias, status: runRow
|
|
432
|
+
return { run: currentAlias, status: runRow.status };
|
|
227
433
|
} finally {
|
|
228
434
|
this.#activeRuns.delete(currentRunId);
|
|
229
435
|
}
|
|
@@ -248,10 +454,7 @@ export default class AgentLoop {
|
|
|
248
454
|
}) {
|
|
249
455
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
250
456
|
if (runRow.status !== 102) {
|
|
251
|
-
await this.#
|
|
252
|
-
id: currentRunId,
|
|
253
|
-
status: 102,
|
|
254
|
-
});
|
|
457
|
+
await this.#setRunStatus(currentRunId, currentAlias, 102);
|
|
255
458
|
}
|
|
256
459
|
|
|
257
460
|
const modelContextSize =
|
|
@@ -267,20 +470,7 @@ export default class AgentLoop {
|
|
|
267
470
|
});
|
|
268
471
|
|
|
269
472
|
let loopIteration = 0;
|
|
270
|
-
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS)
|
|
271
|
-
const healer = new ResponseHealer();
|
|
272
|
-
|
|
273
|
-
let _lastAssembledTokens = 0;
|
|
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);
|
|
473
|
+
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS);
|
|
284
474
|
|
|
285
475
|
await this.#hooks.loop.started.emit({
|
|
286
476
|
runId: currentRunId,
|
|
@@ -292,9 +482,17 @@ export default class AgentLoop {
|
|
|
292
482
|
try {
|
|
293
483
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
294
484
|
if (signal.aborted) {
|
|
295
|
-
|
|
296
|
-
|
|
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,
|
|
297
494
|
status: 499,
|
|
495
|
+
contextSize,
|
|
298
496
|
});
|
|
299
497
|
const out = {
|
|
300
498
|
run: currentAlias,
|
|
@@ -305,6 +503,9 @@ export default class AgentLoop {
|
|
|
305
503
|
return out;
|
|
306
504
|
}
|
|
307
505
|
loopIteration++;
|
|
506
|
+
console.error(
|
|
507
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} ENTER (max=${MAX_LOOP_ITERATIONS})`,
|
|
508
|
+
);
|
|
308
509
|
|
|
309
510
|
let turnPrompt;
|
|
310
511
|
if (loopIteration === 1) {
|
|
@@ -316,6 +517,9 @@ export default class AgentLoop {
|
|
|
316
517
|
);
|
|
317
518
|
}
|
|
318
519
|
|
|
520
|
+
console.error(
|
|
521
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} executing turn`,
|
|
522
|
+
);
|
|
319
523
|
const result = await this.#turnExecutor.execute({
|
|
320
524
|
mode,
|
|
321
525
|
project,
|
|
@@ -327,198 +531,122 @@ export default class AgentLoop {
|
|
|
327
531
|
loopPrompt: turnPrompt,
|
|
328
532
|
loopIteration,
|
|
329
533
|
noRepo,
|
|
534
|
+
noWeb,
|
|
535
|
+
noInteraction,
|
|
536
|
+
noProposals,
|
|
330
537
|
toolSet,
|
|
331
|
-
inRecovery: recovery !== null,
|
|
332
538
|
contextSize,
|
|
333
539
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
334
540
|
signal,
|
|
335
541
|
});
|
|
542
|
+
console.error(
|
|
543
|
+
`[LOOP] ${currentAlias} iter=${loopIteration} turn done: status=${result.status} turn=${result.turn}`,
|
|
544
|
+
);
|
|
336
545
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
id: currentRunId,
|
|
345
|
-
status: 200,
|
|
346
|
-
});
|
|
347
|
-
const out = {
|
|
348
|
-
run: currentAlias,
|
|
349
|
-
status: 413,
|
|
350
|
-
overflow: result.overflow,
|
|
351
|
-
assembledTokens: result.assembledTokens,
|
|
352
|
-
contextSize: result.contextSize,
|
|
353
|
-
turn: result.turn,
|
|
354
|
-
};
|
|
355
|
-
await hook.completed.emit({ projectId, ...out });
|
|
356
|
-
return out;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
_lastAssembledTokens = result.assembledTokens;
|
|
360
|
-
|
|
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;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const runUsage = await this.#db.get_run_usage.get({
|
|
386
|
-
run_id: currentRunId,
|
|
387
|
-
});
|
|
388
|
-
const history = await this.#knownStore.getLog(currentRunId);
|
|
389
|
-
const unknowns = await this.#db.get_unknowns.all({
|
|
390
|
-
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,
|
|
391
553
|
});
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
);
|
|
395
559
|
|
|
396
|
-
await this.#
|
|
560
|
+
await this.#emitRunState({
|
|
397
561
|
projectId,
|
|
398
|
-
|
|
562
|
+
runId: currentRunId,
|
|
563
|
+
alias: currentAlias,
|
|
399
564
|
turn: result.turn,
|
|
400
|
-
status: 102,
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
404
|
-
telemetry: {
|
|
405
|
-
modelAlias: result.modelAlias,
|
|
406
|
-
model: result.model,
|
|
407
|
-
temperature: result.temperature,
|
|
408
|
-
context_size: result.contextSize,
|
|
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,
|
|
416
|
-
prompt_tokens: runUsage.prompt_tokens,
|
|
417
|
-
cached_tokens: runUsage.cached_tokens,
|
|
418
|
-
completion_tokens: runUsage.completion_tokens,
|
|
419
|
-
reasoning_tokens: runUsage.reasoning_tokens,
|
|
420
|
-
total_tokens: runUsage.total_tokens,
|
|
421
|
-
cost: runUsage.cost,
|
|
422
|
-
context_distribution: await this.#db.get_turn_distribution.all({
|
|
423
|
-
run_id: currentRunId,
|
|
424
|
-
turn: result.turn,
|
|
425
|
-
}),
|
|
426
|
-
},
|
|
565
|
+
status: verdict.continue ? 102 : verdict.status,
|
|
566
|
+
contextSize,
|
|
567
|
+
result,
|
|
427
568
|
});
|
|
428
569
|
await this.#hooks.run.step.completed.emit({
|
|
429
570
|
projectId,
|
|
430
571
|
run: currentAlias,
|
|
431
572
|
turn: result.turn,
|
|
432
|
-
flags: result.flags,
|
|
433
573
|
});
|
|
574
|
+
if (verdict.continue) continue;
|
|
434
575
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
await this.#
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
});
|
|
444
|
-
const out = {
|
|
445
|
-
run: currentAlias,
|
|
446
|
-
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,
|
|
447
584
|
turn: result.turn,
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
return out;
|
|
585
|
+
loopId: currentLoopId,
|
|
586
|
+
message: verdict.reason,
|
|
587
|
+
});
|
|
452
588
|
}
|
|
453
|
-
|
|
454
|
-
const progress = healer.assessProgress(result);
|
|
455
|
-
if (progress.continue) continue;
|
|
456
|
-
|
|
457
|
-
await this.#db.update_run_status.run({
|
|
458
|
-
id: currentRunId,
|
|
459
|
-
status: 200,
|
|
460
|
-
});
|
|
461
589
|
const out = {
|
|
462
590
|
run: currentAlias,
|
|
463
|
-
status:
|
|
591
|
+
status: verdict.status,
|
|
464
592
|
turn: result.turn,
|
|
593
|
+
reason: verdict.reason,
|
|
465
594
|
};
|
|
466
595
|
await hook.completed.emit({ projectId, ...out });
|
|
467
596
|
return out;
|
|
468
597
|
}
|
|
469
598
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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,
|
|
473
611
|
});
|
|
474
612
|
const out = {
|
|
475
613
|
run: currentAlias,
|
|
476
|
-
status:
|
|
614
|
+
status: 499,
|
|
477
615
|
turn: loopIteration,
|
|
478
616
|
};
|
|
479
617
|
await hook.completed.emit({ projectId, ...out });
|
|
480
618
|
return out;
|
|
481
619
|
} catch (err) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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}`,
|
|
486
637
|
});
|
|
487
|
-
return { run: currentAlias, status: 499, turn: loopIteration };
|
|
488
638
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
await this.#db.update_run_status.run({
|
|
492
|
-
id: currentRunId,
|
|
493
|
-
status: 500,
|
|
494
|
-
});
|
|
495
|
-
try {
|
|
496
|
-
await this.#knownStore.upsert(
|
|
497
|
-
currentRunId,
|
|
498
|
-
loopIteration,
|
|
499
|
-
`error://${loopIteration}`,
|
|
500
|
-
`${err.message}\n${err.stack}`,
|
|
501
|
-
500,
|
|
502
|
-
{ loopId: currentLoopId },
|
|
503
|
-
);
|
|
504
|
-
} catch {}
|
|
505
|
-
const out = {
|
|
506
|
-
run: currentAlias,
|
|
507
|
-
status: 500,
|
|
508
|
-
turn: loopIteration,
|
|
509
|
-
error: err.message,
|
|
510
|
-
};
|
|
639
|
+
const out = { run: currentAlias, status, turn: loopIteration };
|
|
640
|
+
if (status === 500) out.error = err.message;
|
|
511
641
|
await hook.completed.emit({ projectId, ...out });
|
|
512
642
|
return out;
|
|
513
643
|
} finally {
|
|
514
|
-
await this.#hooks.loop.completed
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
})
|
|
521
|
-
.catch(() => {});
|
|
644
|
+
await this.#hooks.loop.completed.emit({
|
|
645
|
+
runId: currentRunId,
|
|
646
|
+
loopId: currentLoopId,
|
|
647
|
+
mode,
|
|
648
|
+
turns: loopIteration,
|
|
649
|
+
});
|
|
522
650
|
}
|
|
523
651
|
}
|
|
524
652
|
|
|
@@ -530,119 +658,120 @@ export default class AgentLoop {
|
|
|
530
658
|
|
|
531
659
|
const { path, action, output } = resolution;
|
|
532
660
|
|
|
533
|
-
if (action
|
|
534
|
-
|
|
535
|
-
|
|
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({
|
|
536
674
|
runId,
|
|
675
|
+
runRow,
|
|
537
676
|
path,
|
|
538
|
-
attrs,
|
|
539
677
|
output,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
//
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
});
|
|
551
|
-
}
|
|
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
|
+
}
|
|
552
688
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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,
|
|
557
716
|
});
|
|
558
|
-
|
|
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
|
-
|
|
591
|
-
if (path.startsWith("rm://")) {
|
|
592
|
-
if (attrs?.path) {
|
|
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
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (path.startsWith("mv://")) {
|
|
603
|
-
if (attrs?.isMove && attrs?.from) {
|
|
604
|
-
await this.#knownStore.remove(runId, attrs.from);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
717
|
+
return { ok: true, state: "failed", outcome: veto.outcome };
|
|
607
718
|
}
|
|
608
|
-
} else if (action === "reject") {
|
|
609
|
-
await this.#knownStore.resolve(runId, path, 403, output || "rejected");
|
|
610
|
-
} else {
|
|
611
|
-
throw new Error(msg("error.resolution_invalid", { action }));
|
|
612
719
|
}
|
|
613
720
|
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
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
|
+
});
|
|
618
741
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
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 };
|
|
629
752
|
}
|
|
630
753
|
|
|
631
|
-
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
|
+
}
|
|
632
760
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
633
761
|
if (!runRow)
|
|
634
762
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
635
763
|
|
|
636
764
|
const nextTurn = runRow.next_turn;
|
|
637
765
|
|
|
638
|
-
await this.#
|
|
639
|
-
runRow.id,
|
|
640
|
-
nextTurn,
|
|
641
|
-
`prompt://${nextTurn}`,
|
|
642
|
-
message,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
+
});
|
|
646
775
|
|
|
647
776
|
if (this.#activeRuns.has(runRow.id)) {
|
|
648
777
|
return { run: runAlias, status: runRow.status, injected: "next_turn" };
|
|
@@ -652,7 +781,7 @@ export default class AgentLoop {
|
|
|
652
781
|
await this.#db.enqueue_loop.get({
|
|
653
782
|
run_id: runRow.id,
|
|
654
783
|
sequence: injectLoopSeq.sequence,
|
|
655
|
-
mode
|
|
784
|
+
mode,
|
|
656
785
|
model: runRow.model,
|
|
657
786
|
prompt: message,
|
|
658
787
|
config: "{}",
|
|
@@ -660,23 +789,23 @@ export default class AgentLoop {
|
|
|
660
789
|
|
|
661
790
|
const projectId = runRow.project_id;
|
|
662
791
|
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
663
|
-
|
|
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;
|
|
664
803
|
}
|
|
665
804
|
|
|
666
805
|
async getRunHistory(runAlias) {
|
|
667
806
|
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
668
807
|
if (!runRow)
|
|
669
808
|
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
670
|
-
return this.#
|
|
809
|
+
return this.#entries.getLog(runRow.id);
|
|
671
810
|
}
|
|
672
811
|
}
|
|
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";
|