@possumtech/rummy 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +55 -0
- package/LICENSE +21 -0
- package/PLUGINS.md +302 -0
- package/README.md +41 -0
- package/SPEC.md +524 -0
- package/lang/en.json +34 -0
- package/migrations/001_initial_schema.sql +226 -0
- package/package.json +54 -0
- package/service.js +143 -0
- package/src/agent/AgentLoop.js +553 -0
- package/src/agent/ContextAssembler.js +29 -0
- package/src/agent/KnownStore.js +254 -0
- package/src/agent/ProjectAgent.js +101 -0
- package/src/agent/ResponseHealer.js +134 -0
- package/src/agent/TurnExecutor.js +457 -0
- package/src/agent/XmlParser.js +247 -0
- package/src/agent/known_checks.sql +42 -0
- package/src/agent/known_queries.sql +80 -0
- package/src/agent/known_store.sql +161 -0
- package/src/agent/messages.js +17 -0
- package/src/agent/prompt_queue.sql +39 -0
- package/src/agent/runs.sql +114 -0
- package/src/agent/schemes.sql +3 -0
- package/src/agent/sessions.sql +51 -0
- package/src/agent/tokens.js +28 -0
- package/src/agent/turns.sql +36 -0
- package/src/hooks/HookRegistry.js +72 -0
- package/src/hooks/Hooks.js +115 -0
- package/src/hooks/PluginContext.js +116 -0
- package/src/hooks/RummyContext.js +181 -0
- package/src/hooks/ToolRegistry.js +83 -0
- package/src/llm/LlmProvider.js +107 -0
- package/src/llm/OllamaClient.js +88 -0
- package/src/llm/OpenAiClient.js +80 -0
- package/src/llm/OpenRouterClient.js +78 -0
- package/src/llm/XaiClient.js +113 -0
- package/src/plugins/ask_user/README.md +18 -0
- package/src/plugins/ask_user/ask_user.js +48 -0
- package/src/plugins/ask_user/docs.md +2 -0
- package/src/plugins/cp/README.md +18 -0
- package/src/plugins/cp/cp.js +55 -0
- package/src/plugins/cp/docs.md +2 -0
- package/src/plugins/current/README.md +14 -0
- package/src/plugins/current/current.js +48 -0
- package/src/plugins/engine/README.md +12 -0
- package/src/plugins/engine/engine.sql +18 -0
- package/src/plugins/engine/turn_context.sql +51 -0
- package/src/plugins/env/README.md +14 -0
- package/src/plugins/env/docs.md +2 -0
- package/src/plugins/env/env.js +32 -0
- package/src/plugins/file/README.md +25 -0
- package/src/plugins/file/file.js +85 -0
- package/src/plugins/get/README.md +19 -0
- package/src/plugins/get/docs.md +6 -0
- package/src/plugins/get/get.js +53 -0
- package/src/plugins/hedberg/README.md +72 -0
- package/src/plugins/hedberg/docs.md +9 -0
- package/src/plugins/hedberg/edits.js +65 -0
- package/src/plugins/hedberg/hedberg.js +89 -0
- package/src/plugins/hedberg/matcher.js +181 -0
- package/src/plugins/hedberg/normalize.js +41 -0
- package/src/plugins/hedberg/patterns.js +452 -0
- package/src/plugins/hedberg/sed.js +48 -0
- package/src/plugins/helpers.js +22 -0
- package/src/plugins/index.js +180 -0
- package/src/plugins/instructions/README.md +11 -0
- package/src/plugins/instructions/instructions.js +37 -0
- package/src/plugins/instructions/preamble.md +12 -0
- package/src/plugins/known/README.md +18 -0
- package/src/plugins/known/docs.md +3 -0
- package/src/plugins/known/known.js +57 -0
- package/src/plugins/mv/README.md +18 -0
- package/src/plugins/mv/docs.md +2 -0
- package/src/plugins/mv/mv.js +56 -0
- package/src/plugins/previous/README.md +15 -0
- package/src/plugins/previous/previous.js +50 -0
- package/src/plugins/progress/README.md +17 -0
- package/src/plugins/progress/progress.js +44 -0
- package/src/plugins/prompt/README.md +16 -0
- package/src/plugins/prompt/prompt.js +45 -0
- package/src/plugins/rm/README.md +18 -0
- package/src/plugins/rm/docs.md +4 -0
- package/src/plugins/rm/rm.js +51 -0
- package/src/plugins/rpc/README.md +45 -0
- package/src/plugins/rpc/rpc.js +587 -0
- package/src/plugins/set/README.md +32 -0
- package/src/plugins/set/docs.md +4 -0
- package/src/plugins/set/set.js +268 -0
- package/src/plugins/sh/README.md +18 -0
- package/src/plugins/sh/docs.md +2 -0
- package/src/plugins/sh/sh.js +32 -0
- package/src/plugins/skills/README.md +25 -0
- package/src/plugins/skills/skills.js +175 -0
- package/src/plugins/store/README.md +20 -0
- package/src/plugins/store/docs.md +5 -0
- package/src/plugins/store/store.js +52 -0
- package/src/plugins/summarize/README.md +18 -0
- package/src/plugins/summarize/docs.md +4 -0
- package/src/plugins/summarize/summarize.js +24 -0
- package/src/plugins/telemetry/README.md +19 -0
- package/src/plugins/telemetry/rpc_log.sql +28 -0
- package/src/plugins/telemetry/telemetry.js +186 -0
- package/src/plugins/unknown/README.md +23 -0
- package/src/plugins/unknown/docs.md +5 -0
- package/src/plugins/unknown/unknown.js +31 -0
- package/src/plugins/update/README.md +18 -0
- package/src/plugins/update/docs.md +4 -0
- package/src/plugins/update/update.js +24 -0
- package/src/server/ClientConnection.js +228 -0
- package/src/server/RpcRegistry.js +52 -0
- package/src/server/SocketServer.js +43 -0
- package/src/sql/file_constraints.sql +15 -0
- package/src/sql/functions/countTokens.js +7 -0
- package/src/sql/functions/hedmatch.js +8 -0
- package/src/sql/functions/hedreplace.js +8 -0
- package/src/sql/functions/hedsearch.js +8 -0
- package/src/sql/functions/schemeOf.js +7 -0
- package/src/sql/functions/slugify.js +6 -0
- package/src/sql/v_model_context.sql +101 -0
- package/src/sql/v_run_log.sql +23 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import KnownStore from "./KnownStore.js";
|
|
2
|
+
import msg from "./messages.js";
|
|
3
|
+
import ResponseHealer from "./ResponseHealer.js";
|
|
4
|
+
|
|
5
|
+
export default class AgentLoop {
|
|
6
|
+
#db;
|
|
7
|
+
#llmProvider;
|
|
8
|
+
#hooks;
|
|
9
|
+
#turnExecutor;
|
|
10
|
+
#knownStore;
|
|
11
|
+
#activeRuns = new Map();
|
|
12
|
+
|
|
13
|
+
constructor(db, llmProvider, hooks, turnExecutor, knownStore) {
|
|
14
|
+
this.#db = db;
|
|
15
|
+
this.#llmProvider = llmProvider;
|
|
16
|
+
this.#hooks = hooks;
|
|
17
|
+
this.#turnExecutor = turnExecutor;
|
|
18
|
+
this.#knownStore = knownStore;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
abort(runId) {
|
|
22
|
+
const controller = this.#activeRuns.get(runId);
|
|
23
|
+
if (controller) controller.abort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async #generateAlias(modelAlias) {
|
|
27
|
+
return `${modelAlias}_${Date.now()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#buildContinuationPrompt(turn, maxTurns) {
|
|
31
|
+
return `Turn ${turn}/${maxTurns}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async #ensureRun(projectId, model, run, options) {
|
|
35
|
+
const _noContext = options?.noContext === true;
|
|
36
|
+
const isFork = options?.fork === true;
|
|
37
|
+
const requestedModel = model;
|
|
38
|
+
|
|
39
|
+
if (run && isFork) {
|
|
40
|
+
const existingRun = await this.#db.get_run_by_alias.get({ alias: run });
|
|
41
|
+
if (!existingRun)
|
|
42
|
+
throw new Error(msg("error.run_not_found", { runId: run }));
|
|
43
|
+
const alias = await this.#generateAlias(requestedModel);
|
|
44
|
+
const runRow = await this.#db.create_run.get({
|
|
45
|
+
project_id: projectId,
|
|
46
|
+
parent_run_id: existingRun.id,
|
|
47
|
+
model: requestedModel,
|
|
48
|
+
alias,
|
|
49
|
+
temperature: options?.temperature ?? null,
|
|
50
|
+
persona: options?.persona ?? null,
|
|
51
|
+
context_limit: options?.contextLimit ?? null,
|
|
52
|
+
});
|
|
53
|
+
await this.#db.fork_known_entries.run({
|
|
54
|
+
new_run_id: runRow.id,
|
|
55
|
+
parent_run_id: existingRun.id,
|
|
56
|
+
});
|
|
57
|
+
return { runId: runRow.id, alias };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (run) {
|
|
61
|
+
const existingRun = await this.#db.get_run_by_alias.get({ alias: run });
|
|
62
|
+
if (!existingRun)
|
|
63
|
+
throw new Error(msg("error.run_not_found", { runId: run }));
|
|
64
|
+
|
|
65
|
+
const existing = this.#activeRuns.get(existingRun.id);
|
|
66
|
+
if (existing) existing.abort();
|
|
67
|
+
|
|
68
|
+
const unresolved = await this.#knownStore.getUnresolved(existingRun.id);
|
|
69
|
+
if (unresolved.length > 0) {
|
|
70
|
+
return {
|
|
71
|
+
runId: existingRun.id,
|
|
72
|
+
alias: existingRun.alias,
|
|
73
|
+
blocked: true,
|
|
74
|
+
proposed: unresolved,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { runId: existingRun.id, alias: existingRun.alias };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const alias = await this.#generateAlias(requestedModel);
|
|
81
|
+
const runRow = await this.#db.create_run.get({
|
|
82
|
+
project_id: projectId,
|
|
83
|
+
parent_run_id: null,
|
|
84
|
+
model: requestedModel,
|
|
85
|
+
alias,
|
|
86
|
+
temperature: options?.temperature ?? null,
|
|
87
|
+
persona: options?.persona ?? null,
|
|
88
|
+
context_limit: options?.contextLimit ?? null,
|
|
89
|
+
});
|
|
90
|
+
return { runId: runRow.id, alias };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async run(
|
|
94
|
+
mode,
|
|
95
|
+
projectId,
|
|
96
|
+
model,
|
|
97
|
+
prompt,
|
|
98
|
+
projectBufferFiles = null,
|
|
99
|
+
run = null,
|
|
100
|
+
options = {},
|
|
101
|
+
) {
|
|
102
|
+
const hook = mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
103
|
+
await hook.started.emit({
|
|
104
|
+
projectId,
|
|
105
|
+
model,
|
|
106
|
+
prompt,
|
|
107
|
+
projectBufferFiles,
|
|
108
|
+
run,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
112
|
+
if (!project)
|
|
113
|
+
throw new Error(msg("error.project_not_found", { projectId }));
|
|
114
|
+
|
|
115
|
+
const noContext = options?.noContext === true;
|
|
116
|
+
const requestedModel = model;
|
|
117
|
+
|
|
118
|
+
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
119
|
+
if (runInfo.blocked) {
|
|
120
|
+
return {
|
|
121
|
+
run: runInfo.alias,
|
|
122
|
+
status: "proposed",
|
|
123
|
+
remainingCount: runInfo.proposed.length,
|
|
124
|
+
proposed: runInfo.proposed,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
129
|
+
|
|
130
|
+
await this.#db.enqueue_prompt.get({
|
|
131
|
+
run_id: currentRunId,
|
|
132
|
+
mode,
|
|
133
|
+
model: requestedModel,
|
|
134
|
+
prompt: prompt || "",
|
|
135
|
+
config: JSON.stringify({ noContext, temperature: options?.temperature }),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (this.#activeRuns.has(currentRunId)) {
|
|
139
|
+
return { run: currentAlias, status: "queued" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return this.#drainQueue(
|
|
143
|
+
currentRunId,
|
|
144
|
+
currentAlias,
|
|
145
|
+
projectId,
|
|
146
|
+
project,
|
|
147
|
+
options,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
|
|
152
|
+
while (true) {
|
|
153
|
+
const queued = await this.#db.claim_next_prompt.get({
|
|
154
|
+
run_id: currentRunId,
|
|
155
|
+
});
|
|
156
|
+
if (!queued) break;
|
|
157
|
+
|
|
158
|
+
const promptConfig = queued.config ? JSON.parse(queued.config) : {};
|
|
159
|
+
const result = await this.#executeLoop({
|
|
160
|
+
mode: queued.mode,
|
|
161
|
+
project,
|
|
162
|
+
projectId,
|
|
163
|
+
currentRunId,
|
|
164
|
+
currentAlias,
|
|
165
|
+
requestedModel: queued.model,
|
|
166
|
+
prompt: queued.prompt,
|
|
167
|
+
noContext: promptConfig.noContext || false,
|
|
168
|
+
options: { ...options, temperature: promptConfig.temperature },
|
|
169
|
+
hook: queued.mode === "ask" ? this.#hooks.ask : this.#hooks.act,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await this.#db.complete_prompt.run({
|
|
173
|
+
id: queued.id,
|
|
174
|
+
result: JSON.stringify(result),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (result.status === "proposed") return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const runRow = await this.#db.get_run_by_alias.get({ alias: currentAlias });
|
|
181
|
+
return { run: currentAlias, status: runRow?.status || "completed" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async #executeLoop({
|
|
185
|
+
mode,
|
|
186
|
+
project,
|
|
187
|
+
projectId,
|
|
188
|
+
currentRunId,
|
|
189
|
+
currentAlias,
|
|
190
|
+
requestedModel,
|
|
191
|
+
prompt,
|
|
192
|
+
noContext,
|
|
193
|
+
options,
|
|
194
|
+
hook,
|
|
195
|
+
}) {
|
|
196
|
+
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
197
|
+
if (runRow.status !== "running") {
|
|
198
|
+
await this.#db.update_run_status.run({
|
|
199
|
+
id: currentRunId,
|
|
200
|
+
status: "running",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const modelContextSize =
|
|
205
|
+
await this.#llmProvider.getContextSize(requestedModel);
|
|
206
|
+
const contextSize = runRow.context_limit
|
|
207
|
+
? Math.min(runRow.context_limit, modelContextSize)
|
|
208
|
+
: modelContextSize;
|
|
209
|
+
|
|
210
|
+
let loopIteration = 0;
|
|
211
|
+
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
|
|
212
|
+
const healer = new ResponseHealer();
|
|
213
|
+
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
this.#activeRuns.set(currentRunId, controller);
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
219
|
+
if (controller.signal.aborted) {
|
|
220
|
+
await this.#db.update_run_status.run({
|
|
221
|
+
id: currentRunId,
|
|
222
|
+
status: "aborted",
|
|
223
|
+
});
|
|
224
|
+
const out = {
|
|
225
|
+
run: currentAlias,
|
|
226
|
+
status: "aborted",
|
|
227
|
+
turn: loopIteration,
|
|
228
|
+
};
|
|
229
|
+
await hook.completed.emit({ projectId, ...out });
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
loopIteration++;
|
|
233
|
+
|
|
234
|
+
let turnPrompt;
|
|
235
|
+
if (loopIteration === 1) {
|
|
236
|
+
turnPrompt = prompt;
|
|
237
|
+
} else {
|
|
238
|
+
turnPrompt = this.#buildContinuationPrompt(
|
|
239
|
+
loopIteration,
|
|
240
|
+
MAX_LOOP_ITERATIONS,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = await this.#turnExecutor.execute({
|
|
245
|
+
mode,
|
|
246
|
+
project,
|
|
247
|
+
projectId,
|
|
248
|
+
currentRunId,
|
|
249
|
+
currentAlias,
|
|
250
|
+
requestedModel,
|
|
251
|
+
loopPrompt: turnPrompt,
|
|
252
|
+
noContext,
|
|
253
|
+
contextSize,
|
|
254
|
+
options: { ...options, isContinuation: loopIteration > 1 },
|
|
255
|
+
signal: controller.signal,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const runUsage = await this.#db.get_run_usage.get({
|
|
259
|
+
run_id: currentRunId,
|
|
260
|
+
});
|
|
261
|
+
const history = await this.#knownStore.getLog(currentRunId);
|
|
262
|
+
const unknowns = await this.#db.get_unknowns.all({
|
|
263
|
+
run_id: currentRunId,
|
|
264
|
+
});
|
|
265
|
+
const unresolved = await this.#knownStore.getUnresolved(currentRunId);
|
|
266
|
+
|
|
267
|
+
const latestSummary = history
|
|
268
|
+
.filter((e) => e.status === "summary")
|
|
269
|
+
.at(-1);
|
|
270
|
+
|
|
271
|
+
await this.#hooks.run.state.emit({
|
|
272
|
+
projectId,
|
|
273
|
+
run: currentAlias,
|
|
274
|
+
turn: result.turn,
|
|
275
|
+
status: unresolved.length > 0 ? "proposed" : "running",
|
|
276
|
+
summary: latestSummary?.body || "",
|
|
277
|
+
history,
|
|
278
|
+
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
279
|
+
proposed: unresolved.map((p) => ({
|
|
280
|
+
path: p.path,
|
|
281
|
+
type: KnownStore.toolFromPath(p.path) || "unknown",
|
|
282
|
+
attributes: p.attributes ? JSON.parse(p.attributes) : null,
|
|
283
|
+
})),
|
|
284
|
+
telemetry: {
|
|
285
|
+
modelAlias: result.modelAlias,
|
|
286
|
+
model: result.model,
|
|
287
|
+
temperature: result.temperature,
|
|
288
|
+
context_size: result.contextSize,
|
|
289
|
+
prompt_tokens: runUsage.prompt_tokens,
|
|
290
|
+
cached_tokens: runUsage.cached_tokens,
|
|
291
|
+
completion_tokens: runUsage.completion_tokens,
|
|
292
|
+
reasoning_tokens: runUsage.reasoning_tokens,
|
|
293
|
+
total_tokens: runUsage.total_tokens,
|
|
294
|
+
cost: runUsage.cost,
|
|
295
|
+
context_distribution: await this.#db.get_turn_distribution.all({
|
|
296
|
+
run_id: currentRunId,
|
|
297
|
+
turn: result.turn,
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
if (unresolved.length > 0) {
|
|
302
|
+
await this.#db.update_run_status.run({
|
|
303
|
+
id: currentRunId,
|
|
304
|
+
status: "proposed",
|
|
305
|
+
});
|
|
306
|
+
const out = {
|
|
307
|
+
run: currentAlias,
|
|
308
|
+
status: "proposed",
|
|
309
|
+
turn: result.turn,
|
|
310
|
+
proposed: unresolved,
|
|
311
|
+
};
|
|
312
|
+
await hook.completed.emit({ projectId, ...out });
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await this.#hooks.run.step.completed.emit({
|
|
317
|
+
projectId,
|
|
318
|
+
run: currentAlias,
|
|
319
|
+
turn: result.turn,
|
|
320
|
+
flags: result.flags,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const repetition = healer.assessRepetition(result);
|
|
324
|
+
if (!repetition.continue) {
|
|
325
|
+
await this.#db.update_run_status.run({
|
|
326
|
+
id: currentRunId,
|
|
327
|
+
status: "completed",
|
|
328
|
+
});
|
|
329
|
+
const out = {
|
|
330
|
+
run: currentAlias,
|
|
331
|
+
status: "completed",
|
|
332
|
+
turn: result.turn,
|
|
333
|
+
reason: repetition.reason,
|
|
334
|
+
};
|
|
335
|
+
await hook.completed.emit({ projectId, ...out });
|
|
336
|
+
return out;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const progress = healer.assessProgress(result);
|
|
340
|
+
if (progress.continue) continue;
|
|
341
|
+
|
|
342
|
+
await this.#db.update_run_status.run({
|
|
343
|
+
id: currentRunId,
|
|
344
|
+
status: "completed",
|
|
345
|
+
});
|
|
346
|
+
const out = {
|
|
347
|
+
run: currentAlias,
|
|
348
|
+
status: "completed",
|
|
349
|
+
turn: result.turn,
|
|
350
|
+
};
|
|
351
|
+
await hook.completed.emit({ projectId, ...out });
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await this.#db.update_run_status.run({
|
|
356
|
+
id: currentRunId,
|
|
357
|
+
status: "completed",
|
|
358
|
+
});
|
|
359
|
+
const out = {
|
|
360
|
+
run: currentAlias,
|
|
361
|
+
status: "completed",
|
|
362
|
+
turn: loopIteration,
|
|
363
|
+
};
|
|
364
|
+
await hook.completed.emit({ projectId, ...out });
|
|
365
|
+
return out;
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (controller.signal.aborted) {
|
|
368
|
+
await this.#db.update_run_status.run({
|
|
369
|
+
id: currentRunId,
|
|
370
|
+
status: "aborted",
|
|
371
|
+
});
|
|
372
|
+
return { run: currentAlias, status: "aborted", turn: loopIteration };
|
|
373
|
+
}
|
|
374
|
+
console.warn(`[RUMMY] Run failed: ${err.message}`);
|
|
375
|
+
console.warn(`[RUMMY] Stack: ${err.stack}`);
|
|
376
|
+
await this.#db.update_run_status.run({
|
|
377
|
+
id: currentRunId,
|
|
378
|
+
status: "failed",
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
await this.#knownStore.upsert(
|
|
382
|
+
currentRunId,
|
|
383
|
+
loopIteration,
|
|
384
|
+
`error://${loopIteration}`,
|
|
385
|
+
`${err.message}\n${err.stack}`,
|
|
386
|
+
"info",
|
|
387
|
+
);
|
|
388
|
+
} catch {}
|
|
389
|
+
const out = {
|
|
390
|
+
run: currentAlias,
|
|
391
|
+
status: "failed",
|
|
392
|
+
turn: loopIteration,
|
|
393
|
+
error: err.message,
|
|
394
|
+
};
|
|
395
|
+
await hook.completed.emit({ projectId, ...out });
|
|
396
|
+
return out;
|
|
397
|
+
} finally {
|
|
398
|
+
this.#activeRuns.delete(currentRunId);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async resolve(runAlias, resolution) {
|
|
403
|
+
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
404
|
+
if (!runRow)
|
|
405
|
+
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
406
|
+
const runId = runRow.id;
|
|
407
|
+
|
|
408
|
+
const { path, action, output } = resolution;
|
|
409
|
+
|
|
410
|
+
if (action === "accept" || action === "error") {
|
|
411
|
+
const attrs = await this.#knownStore.getAttributes(runId, path);
|
|
412
|
+
const resolvedBody = await this.#composeResolvedContent(
|
|
413
|
+
runId,
|
|
414
|
+
path,
|
|
415
|
+
attrs,
|
|
416
|
+
output,
|
|
417
|
+
);
|
|
418
|
+
const state = action === "error" ? "error" : "pass";
|
|
419
|
+
await this.#knownStore.resolve(runId, path, state, resolvedBody);
|
|
420
|
+
|
|
421
|
+
// Store answer in attributes for ask_user
|
|
422
|
+
if (path.startsWith("ask_user://") && output) {
|
|
423
|
+
const turn = (await this.#db.get_run_by_id.get({ id: runId }))
|
|
424
|
+
.next_turn;
|
|
425
|
+
await this.#knownStore.upsert(runId, turn, path, resolvedBody, state, {
|
|
426
|
+
attributes: { ...attrs, answer: output },
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (action === "accept") {
|
|
431
|
+
if (path.startsWith("rm://")) {
|
|
432
|
+
if (attrs?.path) {
|
|
433
|
+
await this.#knownStore.remove(runId, attrs.path);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (path.startsWith("mv://")) {
|
|
438
|
+
if (attrs?.isMove && attrs?.from) {
|
|
439
|
+
await this.#knownStore.remove(runId, attrs.from);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
} else if (action === "reject") {
|
|
444
|
+
await this.#knownStore.resolve(
|
|
445
|
+
runId,
|
|
446
|
+
path,
|
|
447
|
+
"rejected",
|
|
448
|
+
output || "rejected",
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
throw new Error(msg("error.resolution_invalid", { action }));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const unresolved = await this.#knownStore.getUnresolved(runId);
|
|
455
|
+
if (unresolved.length > 0) {
|
|
456
|
+
return {
|
|
457
|
+
run: runAlias,
|
|
458
|
+
status: "proposed",
|
|
459
|
+
remainingCount: unresolved.length,
|
|
460
|
+
proposed: unresolved,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (await this.#knownStore.hasRejections(runId)) {
|
|
465
|
+
await this.#db.update_run_status.run({ id: runId, status: "completed" });
|
|
466
|
+
return { run: runAlias, status: "completed" };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const hasSummary = await this.#db.get_latest_summary.get({ run_id: runId });
|
|
470
|
+
if (hasSummary?.body) {
|
|
471
|
+
await this.#db.update_run_status.run({ id: runId, status: "completed" });
|
|
472
|
+
return { run: runAlias, status: "completed" };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const latestPrompt = await this.#db.get_latest_prompt.get({
|
|
476
|
+
run_id: runId,
|
|
477
|
+
});
|
|
478
|
+
const resumeMode = latestPrompt?.attributes
|
|
479
|
+
? JSON.parse(latestPrompt.attributes).mode
|
|
480
|
+
: "ask";
|
|
481
|
+
|
|
482
|
+
const projectId = runRow.project_id;
|
|
483
|
+
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
484
|
+
|
|
485
|
+
await this.#db.enqueue_prompt.get({
|
|
486
|
+
run_id: runId,
|
|
487
|
+
mode: resumeMode,
|
|
488
|
+
model: runRow.model,
|
|
489
|
+
prompt: "",
|
|
490
|
+
config: "{}",
|
|
491
|
+
});
|
|
492
|
+
return this.#drainQueue(runId, runAlias, projectId, project, {});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async #composeResolvedContent(runId, path, _attrs, output) {
|
|
496
|
+
const scheme = path.split("://")[0];
|
|
497
|
+
switch (scheme) {
|
|
498
|
+
case "set": {
|
|
499
|
+
const existing = await this.#knownStore.getBody(runId, path);
|
|
500
|
+
return existing || output || "";
|
|
501
|
+
}
|
|
502
|
+
default:
|
|
503
|
+
return output || "";
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async inject(runAlias, message) {
|
|
508
|
+
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
509
|
+
if (!runRow)
|
|
510
|
+
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
511
|
+
|
|
512
|
+
const nextTurn = runRow.next_turn;
|
|
513
|
+
|
|
514
|
+
await this.#knownStore.upsert(
|
|
515
|
+
runRow.id,
|
|
516
|
+
nextTurn,
|
|
517
|
+
`prompt://${nextTurn}`,
|
|
518
|
+
"",
|
|
519
|
+
"info",
|
|
520
|
+
{ attributes: { mode: "ask" } },
|
|
521
|
+
);
|
|
522
|
+
await this.#knownStore.upsert(
|
|
523
|
+
runRow.id,
|
|
524
|
+
nextTurn,
|
|
525
|
+
`ask://${nextTurn}`,
|
|
526
|
+
message,
|
|
527
|
+
"info",
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (this.#activeRuns.has(runRow.id)) {
|
|
531
|
+
return { run: runAlias, status: runRow.status, injected: "next_turn" };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
await this.#db.enqueue_prompt.get({
|
|
535
|
+
run_id: runRow.id,
|
|
536
|
+
mode: "ask",
|
|
537
|
+
model: runRow.model,
|
|
538
|
+
prompt: message,
|
|
539
|
+
config: "{}",
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const projectId = runRow.project_id;
|
|
543
|
+
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
544
|
+
return this.#drainQueue(runRow.id, runAlias, projectId, project, {});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async getRunHistory(runAlias) {
|
|
548
|
+
const runRow = await this.#db.get_run_by_alias.get({ alias: runAlias });
|
|
549
|
+
if (!runRow)
|
|
550
|
+
throw new Error(msg("error.run_not_found", { runId: runAlias }));
|
|
551
|
+
return this.#knownStore.getLog(runRow.id);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin orchestrator. Computes loopStartTurn from the rows,
|
|
3
|
+
* then invokes assembly.system and assembly.user filter chains.
|
|
4
|
+
* All rendering logic lives in plugins.
|
|
5
|
+
*/
|
|
6
|
+
export default class ContextAssembler {
|
|
7
|
+
static async assembleFromTurnContext(
|
|
8
|
+
rows,
|
|
9
|
+
{ type = "ask", systemPrompt = "", contextSize = 0 } = {},
|
|
10
|
+
hooks,
|
|
11
|
+
) {
|
|
12
|
+
// Find loop boundary from active prompt
|
|
13
|
+
const promptEntry = rows.findLast(
|
|
14
|
+
(r) =>
|
|
15
|
+
r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
|
|
16
|
+
);
|
|
17
|
+
const loopStartTurn = promptEntry?.source_turn ?? 0;
|
|
18
|
+
|
|
19
|
+
const ctx = { rows, loopStartTurn, type, contextSize };
|
|
20
|
+
|
|
21
|
+
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|
|
22
|
+
const user = await hooks.assembly.user.filter("", ctx);
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{ role: "system", content: system },
|
|
26
|
+
{ role: "user", content: user },
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
}
|