@possumtech/rummy 0.5.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +306 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +129 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +64 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +525 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +83 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/plugins/yolo/yolo.js +192 -0
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
|
@@ -1,130 +1,19 @@
|
|
|
1
1
|
import RummyContext from "../hooks/RummyContext.js";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { countTokens } from "./tokens.js";
|
|
2
|
+
import { ContextExceededError } from "../llm/errors.js";
|
|
3
|
+
import materializeContext from "./materializeContext.js";
|
|
5
4
|
import XmlParser from "./XmlParser.js";
|
|
6
5
|
|
|
7
|
-
const ACTION_SCHEMES = new Set([
|
|
8
|
-
"get",
|
|
9
|
-
"set",
|
|
10
|
-
"rm",
|
|
11
|
-
"mv",
|
|
12
|
-
"cp",
|
|
13
|
-
"sh",
|
|
14
|
-
"env",
|
|
15
|
-
"search",
|
|
16
|
-
]);
|
|
17
|
-
const MUTATION_SCHEMES = new Set(["set", "rm", "sh", "mv", "cp"]);
|
|
18
|
-
const READ_SCHEMES = new Set(["get", "env", "search"]);
|
|
19
|
-
|
|
20
6
|
export default class TurnExecutor {
|
|
21
7
|
#db;
|
|
22
8
|
#llmProvider;
|
|
23
9
|
#hooks;
|
|
24
|
-
#
|
|
10
|
+
#entries;
|
|
25
11
|
|
|
26
|
-
constructor(db, llmProvider, hooks,
|
|
12
|
+
constructor(db, llmProvider, hooks, entries) {
|
|
27
13
|
this.#db = db;
|
|
28
14
|
this.#llmProvider = llmProvider;
|
|
29
15
|
this.#hooks = hooks;
|
|
30
|
-
this.#
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Rebuild turn_context from v_model_context, then assemble messages.
|
|
35
|
-
* Called at turn start and again after any fidelity demotion within the turn.
|
|
36
|
-
*/
|
|
37
|
-
async #materializeTurnContext({
|
|
38
|
-
runId,
|
|
39
|
-
loopId,
|
|
40
|
-
turn,
|
|
41
|
-
systemPrompt,
|
|
42
|
-
mode,
|
|
43
|
-
toolSet,
|
|
44
|
-
contextSize,
|
|
45
|
-
demoted,
|
|
46
|
-
}) {
|
|
47
|
-
await this.#db.clear_turn_context.run({ run_id: runId, turn });
|
|
48
|
-
const viewRows = await this.#db.get_model_context.all({ run_id: runId });
|
|
49
|
-
for (const row of viewRows) {
|
|
50
|
-
const scheme = row.scheme || "file";
|
|
51
|
-
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
52
|
-
path: row.path,
|
|
53
|
-
scheme,
|
|
54
|
-
body: row.body,
|
|
55
|
-
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
56
|
-
fidelity: row.fidelity,
|
|
57
|
-
category: row.category,
|
|
58
|
-
});
|
|
59
|
-
await this.#db.insert_turn_context.run({
|
|
60
|
-
run_id: runId,
|
|
61
|
-
loop_id: loopId,
|
|
62
|
-
turn,
|
|
63
|
-
ordinal: row.ordinal,
|
|
64
|
-
path: row.path,
|
|
65
|
-
fidelity: row.fidelity,
|
|
66
|
-
status: row.status,
|
|
67
|
-
body: projectedBody ?? "",
|
|
68
|
-
// Full-body token count, not projected. This is the cost to
|
|
69
|
-
// promote the entry — the number the model needs to do Token
|
|
70
|
-
// Budget math. Projecting the demoted symbol-preview (145
|
|
71
|
-
// tokens for a 2108-token file) was misleading the model into
|
|
72
|
-
// promotes that blew the Token Budget by 10-30× per entry.
|
|
73
|
-
tokens: countTokens(row.body ?? ""),
|
|
74
|
-
attributes: row.attributes,
|
|
75
|
-
category: row.category,
|
|
76
|
-
source_turn: row.turn,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
const rows = await this.#db.get_turn_context.all({ run_id: runId, turn });
|
|
80
|
-
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
81
|
-
run_id: runId,
|
|
82
|
-
});
|
|
83
|
-
const lastContextTokens = lastCtx?.context_tokens ?? 0;
|
|
84
|
-
|
|
85
|
-
// Baseline materialization — assemble with model's promoted spending
|
|
86
|
-
// removed (promoted data, promoted logging). The resulting size is the
|
|
87
|
-
// fixed overhead the model can't reduce without further demotion.
|
|
88
|
-
const baselineRows = rows.filter(
|
|
89
|
-
(r) =>
|
|
90
|
-
!(
|
|
91
|
-
(r.category === "data" || r.category === "logging") &&
|
|
92
|
-
r.fidelity === "promoted"
|
|
93
|
-
),
|
|
94
|
-
);
|
|
95
|
-
const baselineMessages = await ContextAssembler.assembleFromTurnContext(
|
|
96
|
-
baselineRows,
|
|
97
|
-
{
|
|
98
|
-
type: mode,
|
|
99
|
-
systemPrompt,
|
|
100
|
-
contextSize,
|
|
101
|
-
demoted,
|
|
102
|
-
toolSet,
|
|
103
|
-
lastContextTokens,
|
|
104
|
-
turn,
|
|
105
|
-
},
|
|
106
|
-
this.#hooks,
|
|
107
|
-
);
|
|
108
|
-
const baselineTokens = baselineMessages.reduce(
|
|
109
|
-
(sum, m) => sum + countTokens(m.content),
|
|
110
|
-
0,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
const messages = await ContextAssembler.assembleFromTurnContext(
|
|
114
|
-
rows,
|
|
115
|
-
{
|
|
116
|
-
type: mode,
|
|
117
|
-
systemPrompt,
|
|
118
|
-
contextSize,
|
|
119
|
-
demoted,
|
|
120
|
-
toolSet,
|
|
121
|
-
lastContextTokens,
|
|
122
|
-
turn,
|
|
123
|
-
baselineTokens,
|
|
124
|
-
},
|
|
125
|
-
this.#hooks,
|
|
126
|
-
);
|
|
127
|
-
return { rows, messages, lastContextTokens };
|
|
16
|
+
this.#entries = entries;
|
|
128
17
|
}
|
|
129
18
|
|
|
130
19
|
async execute({
|
|
@@ -138,24 +27,16 @@ export default class TurnExecutor {
|
|
|
138
27
|
loopPrompt,
|
|
139
28
|
loopIteration,
|
|
140
29
|
noRepo,
|
|
30
|
+
noWeb,
|
|
31
|
+
noInteraction,
|
|
32
|
+
noProposals,
|
|
33
|
+
yolo,
|
|
141
34
|
toolSet,
|
|
142
|
-
inRecovery = false,
|
|
143
35
|
contextSize,
|
|
144
36
|
options,
|
|
145
37
|
signal,
|
|
146
38
|
}) {
|
|
147
|
-
const
|
|
148
|
-
"sh",
|
|
149
|
-
"env",
|
|
150
|
-
"search",
|
|
151
|
-
"ask_user",
|
|
152
|
-
"set",
|
|
153
|
-
]);
|
|
154
|
-
const effectiveToolSet = inRecovery
|
|
155
|
-
? new Set([...toolSet].filter((t) => !RECOVERY_EXCLUDED.has(t)))
|
|
156
|
-
: toolSet;
|
|
157
|
-
|
|
158
|
-
const turn = await this.#knownStore.nextTurn(currentRunId);
|
|
39
|
+
const turn = await this.#entries.nextTurn(currentRunId);
|
|
159
40
|
|
|
160
41
|
const turnRow = await this.#db.create_turn.get({
|
|
161
42
|
run_id: currentRunId,
|
|
@@ -179,7 +60,7 @@ export default class TurnExecutor {
|
|
|
179
60
|
{
|
|
180
61
|
hooks: this.#hooks,
|
|
181
62
|
db: this.#db,
|
|
182
|
-
store: this.#
|
|
63
|
+
store: this.#entries,
|
|
183
64
|
project,
|
|
184
65
|
type: mode,
|
|
185
66
|
sequence: turn,
|
|
@@ -187,13 +68,17 @@ export default class TurnExecutor {
|
|
|
187
68
|
loopId: currentLoopId,
|
|
188
69
|
turnId: turnRow.id,
|
|
189
70
|
noRepo,
|
|
190
|
-
|
|
71
|
+
noWeb,
|
|
72
|
+
noInteraction,
|
|
73
|
+
noProposals,
|
|
74
|
+
yolo,
|
|
75
|
+
toolSet,
|
|
191
76
|
contextSize,
|
|
192
77
|
systemPrompt: null,
|
|
193
78
|
loopPrompt,
|
|
194
79
|
},
|
|
195
80
|
);
|
|
196
|
-
// Plugins write prompt/
|
|
81
|
+
// Plugins write prompt/instructions entries
|
|
197
82
|
await this.#hooks.turn.started.emit({
|
|
198
83
|
rummy,
|
|
199
84
|
mode,
|
|
@@ -205,44 +90,32 @@ export default class TurnExecutor {
|
|
|
205
90
|
await this.#hooks.processTurn(rummy);
|
|
206
91
|
|
|
207
92
|
// Project instructions://system through the instructions tool's projection
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
"instructions://system",
|
|
211
|
-
null,
|
|
212
|
-
);
|
|
213
|
-
const instrAttrs = instrEntry[0]
|
|
214
|
-
? await this.#knownStore.getAttributes(
|
|
215
|
-
currentRunId,
|
|
216
|
-
"instructions://system",
|
|
217
|
-
)
|
|
218
|
-
: null;
|
|
219
|
-
const systemPrompt = await this.#hooks.tools.view("instructions", {
|
|
220
|
-
path: "instructions://system",
|
|
221
|
-
scheme: "instructions",
|
|
222
|
-
body: instrEntry[0]?.body || "",
|
|
223
|
-
attributes: instrAttrs,
|
|
224
|
-
fidelity: "promoted",
|
|
225
|
-
category: "system",
|
|
226
|
-
});
|
|
93
|
+
const systemPrompt =
|
|
94
|
+
await this.#hooks.instructions.resolveSystemPrompt(rummy);
|
|
227
95
|
|
|
228
96
|
// Materialize turn_context: VIEW rows projected through tools
|
|
229
97
|
const demoted = [];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
98
|
+
const budgetCtx = {
|
|
99
|
+
runId: currentRunId,
|
|
100
|
+
loopId: currentLoopId,
|
|
101
|
+
turn,
|
|
102
|
+
systemPrompt,
|
|
103
|
+
mode,
|
|
104
|
+
toolSet,
|
|
105
|
+
demoted,
|
|
106
|
+
loopIteration,
|
|
107
|
+
};
|
|
108
|
+
const initial = await materializeContext({
|
|
109
|
+
db: this.#db,
|
|
110
|
+
hooks: this.#hooks,
|
|
111
|
+
contextSize,
|
|
112
|
+
...budgetCtx,
|
|
113
|
+
});
|
|
241
114
|
|
|
242
115
|
await this.#hooks.context.materialized.emit({
|
|
243
116
|
runId: currentRunId,
|
|
244
117
|
turn,
|
|
245
|
-
rowCount: rows.length,
|
|
118
|
+
rowCount: initial.rows.length,
|
|
246
119
|
});
|
|
247
120
|
|
|
248
121
|
await this.#hooks.run.progress.emit({
|
|
@@ -254,73 +127,26 @@ export default class TurnExecutor {
|
|
|
254
127
|
|
|
255
128
|
const budgetResult = await this.#hooks.budget.enforce({
|
|
256
129
|
contextSize,
|
|
257
|
-
messages,
|
|
258
|
-
rows,
|
|
259
|
-
lastPromptTokens: lastContextTokens,
|
|
130
|
+
messages: initial.messages,
|
|
131
|
+
rows: initial.rows,
|
|
132
|
+
lastPromptTokens: initial.lastContextTokens,
|
|
133
|
+
ctx: budgetCtx,
|
|
134
|
+
rummy,
|
|
260
135
|
});
|
|
261
|
-
messages = budgetResult.messages;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
currentRunId,
|
|
276
|
-
promptRow.path,
|
|
277
|
-
"demoted",
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
const reMat = await this.#materializeTurnContext({
|
|
281
|
-
runId: currentRunId,
|
|
282
|
-
loopId: currentLoopId,
|
|
283
|
-
turn,
|
|
284
|
-
systemPrompt,
|
|
285
|
-
mode,
|
|
286
|
-
toolSet: effectiveToolSet,
|
|
287
|
-
contextSize,
|
|
288
|
-
demoted,
|
|
289
|
-
});
|
|
290
|
-
rows = reMat.rows;
|
|
291
|
-
messages = reMat.messages;
|
|
292
|
-
const recheck = await this.#hooks.budget.enforce({
|
|
293
|
-
contextSize,
|
|
294
|
-
messages,
|
|
295
|
-
rows,
|
|
296
|
-
lastPromptTokens: reMat.lastContextTokens,
|
|
297
|
-
});
|
|
298
|
-
messages = recheck.messages;
|
|
299
|
-
rows = recheck.rows;
|
|
300
|
-
assembledTokens =
|
|
301
|
-
recheck.assembledTokens ??
|
|
302
|
-
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
303
|
-
if (recheck.status === 413) {
|
|
304
|
-
return {
|
|
305
|
-
turn,
|
|
306
|
-
turnId: turnRow.id,
|
|
307
|
-
status: 413,
|
|
308
|
-
assembledTokens,
|
|
309
|
-
contextSize,
|
|
310
|
-
overflow: recheck.overflow,
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
} else {
|
|
314
|
-
// Base context too large even without new prompt — genuine failure
|
|
315
|
-
return {
|
|
316
|
-
turn,
|
|
317
|
-
turnId: turnRow.id,
|
|
318
|
-
status: 413,
|
|
319
|
-
assembledTokens,
|
|
320
|
-
contextSize,
|
|
321
|
-
overflow: budgetResult.overflow,
|
|
322
|
-
};
|
|
323
|
-
}
|
|
136
|
+
const messages = budgetResult.messages;
|
|
137
|
+
const assembledTokens = budgetResult.assembledTokens;
|
|
138
|
+
|
|
139
|
+
if (!budgetResult.ok) {
|
|
140
|
+
return {
|
|
141
|
+
turn,
|
|
142
|
+
turnId: turnRow.id,
|
|
143
|
+
recorded: [],
|
|
144
|
+
summaryText: null,
|
|
145
|
+
updateText: null,
|
|
146
|
+
assembledTokens,
|
|
147
|
+
contextSize,
|
|
148
|
+
overflow: budgetResult.overflow,
|
|
149
|
+
};
|
|
324
150
|
}
|
|
325
151
|
|
|
326
152
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
@@ -332,49 +158,37 @@ export default class TurnExecutor {
|
|
|
332
158
|
turn,
|
|
333
159
|
});
|
|
334
160
|
|
|
335
|
-
// Call LLM
|
|
161
|
+
// Call LLM. Transient-error retry + context-exceeded detection live
|
|
162
|
+
// in LlmProvider; context-exceeded surfaces as ContextExceededError.
|
|
336
163
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
337
164
|
let rawResult;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
/\b(context.*(size|length|limit)|token.*(limit|exceed)|too.*(long|large))\b/i.test(
|
|
344
|
-
e.message,
|
|
165
|
+
try {
|
|
166
|
+
rawResult = await this.#llmProvider.completion(
|
|
167
|
+
filteredMessages,
|
|
168
|
+
requestedModel,
|
|
169
|
+
{ temperature: options?.temperature, signal },
|
|
345
170
|
);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
console.warn(
|
|
366
|
-
`[RUMMY] LLM context exceeded: ${err.message.slice(0, 120)}. Returning 413.`,
|
|
367
|
-
);
|
|
368
|
-
return {
|
|
369
|
-
turn,
|
|
370
|
-
turnId: turnRow.id,
|
|
371
|
-
status: 413,
|
|
372
|
-
assembledTokens,
|
|
373
|
-
contextSize,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
throw err;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err instanceof ContextExceededError) {
|
|
173
|
+
await this.#hooks.error.log.emit({
|
|
174
|
+
store: this.#entries,
|
|
175
|
+
runId: currentRunId,
|
|
176
|
+
turn,
|
|
177
|
+
loopId: currentLoopId,
|
|
178
|
+
message: `LLM context exceeded: ${err.message}`,
|
|
179
|
+
status: 413,
|
|
180
|
+
});
|
|
181
|
+
return {
|
|
182
|
+
turn,
|
|
183
|
+
turnId: turnRow.id,
|
|
184
|
+
recorded: [],
|
|
185
|
+
summaryText: null,
|
|
186
|
+
updateText: null,
|
|
187
|
+
assembledTokens,
|
|
188
|
+
contextSize,
|
|
189
|
+
};
|
|
377
190
|
}
|
|
191
|
+
throw err;
|
|
378
192
|
}
|
|
379
193
|
const result = await this.#hooks.llm.response.filter(rawResult, {
|
|
380
194
|
model: requestedModel,
|
|
@@ -387,7 +201,10 @@ export default class TurnExecutor {
|
|
|
387
201
|
usage: result.usage,
|
|
388
202
|
});
|
|
389
203
|
const responseMessage = result.choices?.[0]?.message;
|
|
390
|
-
|
|
204
|
+
// A valid completion response always carries content (possibly
|
|
205
|
+
// empty) on the message; protect against that specific case so
|
|
206
|
+
// downstream parsers see a string.
|
|
207
|
+
const content = responseMessage?.content ? responseMessage.content : "";
|
|
391
208
|
|
|
392
209
|
await this.#hooks.run.progress.emit({
|
|
393
210
|
projectId,
|
|
@@ -397,19 +214,38 @@ export default class TurnExecutor {
|
|
|
397
214
|
});
|
|
398
215
|
|
|
399
216
|
// Parse and emit — plugins handle audit storage
|
|
400
|
-
const { commands, unparsed } = XmlParser.parse(content);
|
|
217
|
+
const { commands, warnings, unparsed } = XmlParser.parse(content);
|
|
218
|
+
for (const w of warnings) {
|
|
219
|
+
await this.#hooks.error.log.emit({
|
|
220
|
+
store: this.#entries,
|
|
221
|
+
runId: currentRunId,
|
|
222
|
+
turn,
|
|
223
|
+
message: w,
|
|
224
|
+
loopId: currentLoopId,
|
|
225
|
+
status: 422,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (commands.length === 0 && !!unparsed?.trim() && warnings.length === 0) {
|
|
229
|
+
await this.#hooks.error.log.emit({
|
|
230
|
+
store: this.#entries,
|
|
231
|
+
runId: currentRunId,
|
|
232
|
+
turn,
|
|
233
|
+
loopId: currentLoopId,
|
|
234
|
+
message: "Response contained no actionable tags.",
|
|
235
|
+
status: 422,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
401
238
|
|
|
402
|
-
//
|
|
239
|
+
// Merge reasoning contributions from subscribers (think plugin's
|
|
240
|
+
// <think> tag, other plugin reasoning sources). Filter starts with
|
|
241
|
+
// the API-provided reasoning_content and layers on each plugin's
|
|
242
|
+
// contribution.
|
|
403
243
|
if (responseMessage) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const apiReasoning = responseMessage.reasoning_content || "";
|
|
410
|
-
const parts = [apiReasoning, thinkText].filter(Boolean);
|
|
411
|
-
responseMessage.reasoning_content =
|
|
412
|
-
parts.length > 0 ? parts.join("\n") : null;
|
|
244
|
+
const seed = responseMessage.reasoning_content
|
|
245
|
+
? responseMessage.reasoning_content
|
|
246
|
+
: "";
|
|
247
|
+
const merged = await this.#hooks.llm.reasoning.filter(seed, { commands });
|
|
248
|
+
responseMessage.reasoning_content = merged ? merged : null;
|
|
413
249
|
}
|
|
414
250
|
|
|
415
251
|
const systemMsg = filteredMessages.find((m) => m.role === "system");
|
|
@@ -445,171 +281,127 @@ export default class TurnExecutor {
|
|
|
445
281
|
// Sequential queue. Each tool completes before the next starts.
|
|
446
282
|
// On failure: abort remaining. On proposal: notify client, await
|
|
447
283
|
// resolution, continue.
|
|
448
|
-
|
|
284
|
+
// Narration text outside tags is fine when the turn also emitted
|
|
285
|
+
// at least one command — "OK", "Let me check:", reasoning prefixes
|
|
286
|
+
// are natural. Parse warnings and no-tags responses already emitted
|
|
287
|
+
// errors above; dispatch crashes and failed entries emit below.
|
|
449
288
|
let abortAfter = null;
|
|
450
289
|
|
|
451
290
|
for (const entry of recorded) {
|
|
291
|
+
if (entry.state === "failed" || entry.state === "cancelled") continue;
|
|
292
|
+
|
|
452
293
|
if (abortAfter) {
|
|
453
294
|
const errorMsg = `Aborted — preceding <${abortAfter}> failed.`;
|
|
454
|
-
await this.#
|
|
455
|
-
currentRunId,
|
|
295
|
+
await this.#entries.set({
|
|
296
|
+
runId: currentRunId,
|
|
456
297
|
turn,
|
|
457
|
-
entry.resultPath || entry.path,
|
|
458
|
-
errorMsg,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
298
|
+
path: entry.resultPath || entry.path,
|
|
299
|
+
body: errorMsg,
|
|
300
|
+
state: "failed",
|
|
301
|
+
outcome: "aborted",
|
|
302
|
+
attributes: { error: errorMsg },
|
|
303
|
+
loopId: currentLoopId,
|
|
304
|
+
});
|
|
463
305
|
continue;
|
|
464
306
|
}
|
|
465
307
|
|
|
466
308
|
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
467
|
-
|
|
309
|
+
try {
|
|
310
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
311
|
+
} catch (dispatchErr) {
|
|
312
|
+
await this.#hooks.error.log.emit({
|
|
313
|
+
store: this.#entries,
|
|
314
|
+
runId: currentRunId,
|
|
315
|
+
turn,
|
|
316
|
+
loopId: currentLoopId,
|
|
317
|
+
message: `Dispatch crash in ${entry.scheme}: ${dispatchErr.message}`,
|
|
318
|
+
status: 500,
|
|
319
|
+
});
|
|
320
|
+
abortAfter = entry.scheme;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
468
323
|
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
469
324
|
await this.#hooks.entry.created.emit(entry);
|
|
470
325
|
|
|
471
|
-
//
|
|
472
|
-
|
|
326
|
+
// Plugins (e.g. set) materialize pending proposals from the
|
|
327
|
+
// recorded entry — e.g. search/replace revisions → set:// 202.
|
|
328
|
+
await this.#hooks.proposal.prepare.emit({ rummy, recorded: [entry] });
|
|
473
329
|
|
|
474
330
|
// Check for any proposals created by this entry's dispatch
|
|
475
|
-
const proposed = await this.#
|
|
331
|
+
const proposed = await this.#entries.getUnresolved(currentRunId);
|
|
476
332
|
for (const p of proposed) {
|
|
477
|
-
await this.#hooks.
|
|
333
|
+
await this.#hooks.proposal.pending.emit({
|
|
478
334
|
projectId,
|
|
479
335
|
run: currentAlias,
|
|
480
336
|
proposed: [p],
|
|
337
|
+
rummy,
|
|
481
338
|
});
|
|
482
|
-
await this.#
|
|
483
|
-
const resolved = await this.#
|
|
484
|
-
run_id: currentRunId,
|
|
485
|
-
path: p.path,
|
|
486
|
-
});
|
|
339
|
+
await this.#entries.waitForResolution(currentRunId, p.path);
|
|
340
|
+
const resolved = await this.#entries.getState(currentRunId, p.path);
|
|
487
341
|
if (resolved?.status >= 400) {
|
|
488
|
-
|
|
342
|
+
await this.#hooks.error.log.emit({
|
|
343
|
+
store: this.#entries,
|
|
344
|
+
runId: currentRunId,
|
|
345
|
+
turn,
|
|
346
|
+
loopId: currentLoopId,
|
|
347
|
+
message: `Proposal ${p.path} rejected: status ${resolved.status}.`,
|
|
348
|
+
status: resolved.status,
|
|
349
|
+
});
|
|
489
350
|
abortAfter = entry.scheme;
|
|
490
351
|
}
|
|
491
352
|
}
|
|
492
353
|
|
|
493
|
-
|
|
494
|
-
if (!hasErrors) {
|
|
354
|
+
if (!abortAfter) {
|
|
495
355
|
const entryPath = entry.resultPath || entry.path;
|
|
496
|
-
const row = await this.#
|
|
497
|
-
run_id: currentRunId,
|
|
498
|
-
path: entryPath,
|
|
499
|
-
});
|
|
356
|
+
const row = await this.#entries.getState(currentRunId, entryPath);
|
|
500
357
|
if (row?.status >= 400) {
|
|
501
|
-
|
|
358
|
+
await this.#hooks.error.log.emit({
|
|
359
|
+
store: this.#entries,
|
|
360
|
+
runId: currentRunId,
|
|
361
|
+
turn,
|
|
362
|
+
loopId: currentLoopId,
|
|
363
|
+
message: `Entry ${entryPath} failed: status ${row.status}.`,
|
|
364
|
+
status: row.status,
|
|
365
|
+
});
|
|
502
366
|
abortAfter = entry.scheme;
|
|
503
367
|
}
|
|
504
368
|
}
|
|
505
369
|
}
|
|
506
370
|
|
|
507
|
-
// Turn Demotion:
|
|
508
|
-
// turn's
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
runId: currentRunId,
|
|
516
|
-
loopId: currentLoopId,
|
|
517
|
-
turn,
|
|
518
|
-
systemPrompt,
|
|
519
|
-
mode,
|
|
520
|
-
toolSet: effectiveToolSet,
|
|
521
|
-
contextSize,
|
|
522
|
-
demoted,
|
|
523
|
-
});
|
|
524
|
-
budgetRecovery = await this.#hooks.budget.postDispatch({
|
|
525
|
-
contextSize,
|
|
526
|
-
messages: postMat.messages,
|
|
527
|
-
rows: postMat.rows,
|
|
528
|
-
runId: currentRunId,
|
|
529
|
-
loopId: currentLoopId,
|
|
530
|
-
turn,
|
|
531
|
-
db: this.#db,
|
|
532
|
-
store: this.#knownStore,
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
const summaryEntry = recorded.findLast((e) => e.scheme === "summarize");
|
|
537
|
-
const updateEntry = recorded.findLast((e) => e.scheme === "update");
|
|
538
|
-
let summaryText = summaryEntry?.body || null;
|
|
539
|
-
let updateText = updateEntry?.body || null;
|
|
540
|
-
|
|
541
|
-
// If model sent both, last signal wins — respects the model's final intent
|
|
542
|
-
if (summaryText && updateText) {
|
|
543
|
-
const lastLifecycle = recorded.findLast(
|
|
544
|
-
(e) => e.scheme === "summarize" || e.scheme === "update",
|
|
545
|
-
);
|
|
546
|
-
if (lastLifecycle.scheme === "summarize") updateText = null;
|
|
547
|
-
else summaryText = null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// If model says "done" but actions failed, override — the model's
|
|
551
|
-
// assertion that it's done is false if it failed to do what it tried.
|
|
552
|
-
if (summaryText && hasErrors) {
|
|
553
|
-
console.warn(
|
|
554
|
-
"[RUMMY] Overriding <summarize> — actions in this turn failed. Continuing.",
|
|
555
|
-
);
|
|
556
|
-
// Mark the recorded summarize entry as 409 so the model sees it was rejected
|
|
557
|
-
if (summaryEntry?.path) {
|
|
558
|
-
await this.#knownStore.resolve(
|
|
559
|
-
currentRunId,
|
|
560
|
-
summaryEntry.path,
|
|
561
|
-
409,
|
|
562
|
-
"Overridden — actions in this turn failed. Use <update/> until resolved.",
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
updateText = summaryText;
|
|
566
|
-
summaryText = null;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// If model sent neither, heal from content
|
|
570
|
-
let statusHealed = false;
|
|
571
|
-
if (!summaryText && !updateText) {
|
|
572
|
-
const healed = ResponseHealer.healStatus(content, commands);
|
|
573
|
-
summaryText = healed.summaryText;
|
|
574
|
-
updateText = healed.updateText;
|
|
575
|
-
statusHealed = true;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// --- Classify for return value ---
|
|
579
|
-
|
|
580
|
-
const actionCalls = recorded.filter((e) => ACTION_SCHEMES.has(e.scheme));
|
|
581
|
-
const writeCalls = recorded.filter(
|
|
582
|
-
(e) =>
|
|
583
|
-
e.scheme === "known" ||
|
|
584
|
-
(e.scheme === "set" && !e.attributes?.blocks && !e.attributes?.search),
|
|
585
|
-
);
|
|
586
|
-
const unknownCalls = recorded.filter((e) => e.scheme === "unknown");
|
|
371
|
+
// Turn Demotion: budget plugin re-materializes end-of-turn context
|
|
372
|
+
// and demotes this turn's promoted entries on overflow. Overflow
|
|
373
|
+
// emits an error (status 413) via the unified error channel.
|
|
374
|
+
await this.#hooks.budget.postDispatch({
|
|
375
|
+
contextSize,
|
|
376
|
+
ctx: budgetCtx,
|
|
377
|
+
rummy,
|
|
378
|
+
});
|
|
587
379
|
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
380
|
+
const { summaryText, updateText } = await this.#hooks.update.resolve({
|
|
381
|
+
recorded,
|
|
382
|
+
content,
|
|
383
|
+
commands,
|
|
384
|
+
runId: currentRunId,
|
|
385
|
+
turn,
|
|
386
|
+
loopId: currentLoopId,
|
|
387
|
+
rummy,
|
|
388
|
+
});
|
|
592
389
|
|
|
593
390
|
const askUserEntry = recorded.find((e) => e.scheme === "ask_user");
|
|
594
391
|
|
|
595
392
|
const turnResult = {
|
|
596
393
|
turn,
|
|
597
394
|
turnId: turnRow.id,
|
|
598
|
-
|
|
599
|
-
writeCalls,
|
|
600
|
-
unknownCalls,
|
|
395
|
+
recorded,
|
|
601
396
|
summaryText,
|
|
602
397
|
updateText,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
flags,
|
|
606
|
-
model: result.model || requestedModel,
|
|
398
|
+
askUserCmd: askUserEntry,
|
|
399
|
+
model: result.model ? result.model : requestedModel,
|
|
607
400
|
modelAlias: requestedModel,
|
|
608
401
|
temperature: options?.temperature,
|
|
609
402
|
contextSize,
|
|
610
403
|
assembledTokens,
|
|
611
404
|
usage: result.usage,
|
|
612
|
-
budgetRecovery,
|
|
613
405
|
};
|
|
614
406
|
|
|
615
407
|
await this.#hooks.turn.completed.emit(turnResult);
|
|
@@ -623,59 +415,78 @@ export default class TurnExecutor {
|
|
|
623
415
|
*/
|
|
624
416
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
625
417
|
const scheme = cmd.name;
|
|
626
|
-
|
|
418
|
+
// Each tool's XmlParser shape surfaces exactly one of these
|
|
419
|
+
// three fields as its addressable target. Treat absent as empty
|
|
420
|
+
// so the length/control-char validation below catches bad shapes
|
|
421
|
+
// rather than letting an undefined slip through.
|
|
422
|
+
let rawTarget = "";
|
|
423
|
+
if (cmd.path) rawTarget = cmd.path;
|
|
424
|
+
else if (cmd.command) rawTarget = cmd.command;
|
|
425
|
+
else if (cmd.question) rawTarget = cmd.question;
|
|
627
426
|
// Reject paths that are likely reasoning bleed — too long or contain non-printing chars
|
|
628
427
|
if (rawTarget.length > 512 || /\p{Cc}/u.test(rawTarget)) {
|
|
629
|
-
const rejectPath = await this.#
|
|
428
|
+
const rejectPath = await this.#entries.logPath(
|
|
630
429
|
runId,
|
|
631
|
-
scheme,
|
|
632
|
-
`${scheme}://invalid`,
|
|
633
430
|
turn,
|
|
431
|
+
scheme,
|
|
432
|
+
"invalid",
|
|
634
433
|
);
|
|
635
|
-
await this.#
|
|
434
|
+
await this.#entries.set({
|
|
636
435
|
runId,
|
|
637
436
|
turn,
|
|
638
|
-
rejectPath,
|
|
639
|
-
`Invalid path: too long or contains non-printing characters`,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
437
|
+
path: rejectPath,
|
|
438
|
+
body: `Invalid path: too long or contains non-printing characters`,
|
|
439
|
+
state: "failed",
|
|
440
|
+
outcome: "validation",
|
|
441
|
+
attributes: { action: scheme },
|
|
442
|
+
loopId,
|
|
443
|
+
});
|
|
643
444
|
return {
|
|
644
445
|
scheme,
|
|
645
446
|
path: rejectPath,
|
|
646
447
|
body: "",
|
|
647
448
|
attributes: {},
|
|
648
|
-
|
|
449
|
+
state: "failed",
|
|
450
|
+
outcome: "validation",
|
|
649
451
|
resultPath: rejectPath,
|
|
650
452
|
};
|
|
651
453
|
}
|
|
652
454
|
const target = rawTarget;
|
|
653
|
-
const resultPath = await this.#
|
|
654
|
-
runId,
|
|
655
|
-
scheme,
|
|
656
|
-
target,
|
|
657
|
-
turn,
|
|
658
|
-
);
|
|
455
|
+
const resultPath = await this.#entries.logPath(runId, turn, scheme, target);
|
|
659
456
|
|
|
660
457
|
// Pass parsed command fields through as attributes
|
|
661
458
|
const { name: _, ...attributes } = cmd;
|
|
662
459
|
if (cmd.path) attributes.path = target;
|
|
663
460
|
|
|
664
|
-
|
|
461
|
+
// Same per-shape resolution as rawTarget; the three sources are
|
|
462
|
+
// mutually exclusive per tool. Empty string when none set.
|
|
463
|
+
let body = "";
|
|
464
|
+
if (cmd.body) body = cmd.body;
|
|
465
|
+
else if (cmd.command) body = cmd.command;
|
|
466
|
+
else if (cmd.question) body = cmd.question;
|
|
665
467
|
|
|
666
468
|
// Filter: plugins can validate/transform before recording
|
|
667
469
|
const filtered = await this.#hooks.entry.recording.filter(
|
|
668
|
-
{
|
|
669
|
-
|
|
470
|
+
{
|
|
471
|
+
scheme,
|
|
472
|
+
path: resultPath,
|
|
473
|
+
body,
|
|
474
|
+
attributes,
|
|
475
|
+
state: "resolved",
|
|
476
|
+
outcome: null,
|
|
477
|
+
},
|
|
478
|
+
{ store: this.#entries, runId, turn, loopId, mode },
|
|
670
479
|
);
|
|
671
|
-
if (filtered.
|
|
480
|
+
if (filtered.state === "failed" || filtered.state === "cancelled") {
|
|
481
|
+
return filtered;
|
|
482
|
+
}
|
|
672
483
|
|
|
673
484
|
return {
|
|
674
485
|
scheme: filtered.scheme,
|
|
675
486
|
path: filtered.path,
|
|
676
487
|
body: filtered.body,
|
|
677
488
|
attributes: filtered.attributes,
|
|
678
|
-
|
|
489
|
+
state: "resolved",
|
|
679
490
|
resultPath: filtered.path,
|
|
680
491
|
};
|
|
681
492
|
}
|