@possumtech/rummy 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +21 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -330
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +229 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -125
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +135 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +260 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +122 -9
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +67 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +58 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
|
@@ -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,15 @@ export default class TurnExecutor {
|
|
|
138
27
|
loopPrompt,
|
|
139
28
|
loopIteration,
|
|
140
29
|
noRepo,
|
|
30
|
+
noWeb,
|
|
31
|
+
noInteraction,
|
|
32
|
+
noProposals,
|
|
141
33
|
toolSet,
|
|
142
|
-
inRecovery = false,
|
|
143
34
|
contextSize,
|
|
144
35
|
options,
|
|
145
36
|
signal,
|
|
146
37
|
}) {
|
|
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);
|
|
38
|
+
const turn = await this.#entries.nextTurn(currentRunId);
|
|
159
39
|
|
|
160
40
|
const turnRow = await this.#db.create_turn.get({
|
|
161
41
|
run_id: currentRunId,
|
|
@@ -179,7 +59,7 @@ export default class TurnExecutor {
|
|
|
179
59
|
{
|
|
180
60
|
hooks: this.#hooks,
|
|
181
61
|
db: this.#db,
|
|
182
|
-
store: this.#
|
|
62
|
+
store: this.#entries,
|
|
183
63
|
project,
|
|
184
64
|
type: mode,
|
|
185
65
|
sequence: turn,
|
|
@@ -187,13 +67,16 @@ export default class TurnExecutor {
|
|
|
187
67
|
loopId: currentLoopId,
|
|
188
68
|
turnId: turnRow.id,
|
|
189
69
|
noRepo,
|
|
190
|
-
|
|
70
|
+
noWeb,
|
|
71
|
+
noInteraction,
|
|
72
|
+
noProposals,
|
|
73
|
+
toolSet,
|
|
191
74
|
contextSize,
|
|
192
75
|
systemPrompt: null,
|
|
193
76
|
loopPrompt,
|
|
194
77
|
},
|
|
195
78
|
);
|
|
196
|
-
// Plugins write prompt/
|
|
79
|
+
// Plugins write prompt/instructions entries
|
|
197
80
|
await this.#hooks.turn.started.emit({
|
|
198
81
|
rummy,
|
|
199
82
|
mode,
|
|
@@ -205,44 +88,32 @@ export default class TurnExecutor {
|
|
|
205
88
|
await this.#hooks.processTurn(rummy);
|
|
206
89
|
|
|
207
90
|
// 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
|
-
});
|
|
91
|
+
const systemPrompt =
|
|
92
|
+
await this.#hooks.instructions.resolveSystemPrompt(rummy);
|
|
227
93
|
|
|
228
94
|
// Materialize turn_context: VIEW rows projected through tools
|
|
229
95
|
const demoted = [];
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
96
|
+
const budgetCtx = {
|
|
97
|
+
runId: currentRunId,
|
|
98
|
+
loopId: currentLoopId,
|
|
99
|
+
turn,
|
|
100
|
+
systemPrompt,
|
|
101
|
+
mode,
|
|
102
|
+
toolSet,
|
|
103
|
+
demoted,
|
|
104
|
+
loopIteration,
|
|
105
|
+
};
|
|
106
|
+
const initial = await materializeContext({
|
|
107
|
+
db: this.#db,
|
|
108
|
+
hooks: this.#hooks,
|
|
109
|
+
contextSize,
|
|
110
|
+
...budgetCtx,
|
|
111
|
+
});
|
|
241
112
|
|
|
242
113
|
await this.#hooks.context.materialized.emit({
|
|
243
114
|
runId: currentRunId,
|
|
244
115
|
turn,
|
|
245
|
-
rowCount: rows.length,
|
|
116
|
+
rowCount: initial.rows.length,
|
|
246
117
|
});
|
|
247
118
|
|
|
248
119
|
await this.#hooks.run.progress.emit({
|
|
@@ -254,73 +125,26 @@ export default class TurnExecutor {
|
|
|
254
125
|
|
|
255
126
|
const budgetResult = await this.#hooks.budget.enforce({
|
|
256
127
|
contextSize,
|
|
257
|
-
messages,
|
|
258
|
-
rows,
|
|
259
|
-
lastPromptTokens: lastContextTokens,
|
|
128
|
+
messages: initial.messages,
|
|
129
|
+
rows: initial.rows,
|
|
130
|
+
lastPromptTokens: initial.lastContextTokens,
|
|
131
|
+
ctx: budgetCtx,
|
|
132
|
+
rummy,
|
|
260
133
|
});
|
|
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
|
-
}
|
|
134
|
+
const messages = budgetResult.messages;
|
|
135
|
+
const assembledTokens = budgetResult.assembledTokens;
|
|
136
|
+
|
|
137
|
+
if (!budgetResult.ok) {
|
|
138
|
+
return {
|
|
139
|
+
turn,
|
|
140
|
+
turnId: turnRow.id,
|
|
141
|
+
recorded: [],
|
|
142
|
+
summaryText: null,
|
|
143
|
+
updateText: null,
|
|
144
|
+
assembledTokens,
|
|
145
|
+
contextSize,
|
|
146
|
+
overflow: budgetResult.overflow,
|
|
147
|
+
};
|
|
324
148
|
}
|
|
325
149
|
|
|
326
150
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
@@ -332,49 +156,37 @@ export default class TurnExecutor {
|
|
|
332
156
|
turn,
|
|
333
157
|
});
|
|
334
158
|
|
|
335
|
-
// Call LLM
|
|
159
|
+
// Call LLM. Transient-error retry + context-exceeded detection live
|
|
160
|
+
// in LlmProvider; context-exceeded surfaces as ContextExceededError.
|
|
336
161
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
337
162
|
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,
|
|
163
|
+
try {
|
|
164
|
+
rawResult = await this.#llmProvider.completion(
|
|
165
|
+
filteredMessages,
|
|
166
|
+
requestedModel,
|
|
167
|
+
{ temperature: options?.temperature, signal },
|
|
345
168
|
);
|
|
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;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err instanceof ContextExceededError) {
|
|
171
|
+
await this.#hooks.error.log.emit({
|
|
172
|
+
store: this.#entries,
|
|
173
|
+
runId: currentRunId,
|
|
174
|
+
turn,
|
|
175
|
+
loopId: currentLoopId,
|
|
176
|
+
message: `LLM context exceeded: ${err.message}`,
|
|
177
|
+
status: 413,
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
turn,
|
|
181
|
+
turnId: turnRow.id,
|
|
182
|
+
recorded: [],
|
|
183
|
+
summaryText: null,
|
|
184
|
+
updateText: null,
|
|
185
|
+
assembledTokens,
|
|
186
|
+
contextSize,
|
|
187
|
+
};
|
|
377
188
|
}
|
|
189
|
+
throw err;
|
|
378
190
|
}
|
|
379
191
|
const result = await this.#hooks.llm.response.filter(rawResult, {
|
|
380
192
|
model: requestedModel,
|
|
@@ -387,7 +199,10 @@ export default class TurnExecutor {
|
|
|
387
199
|
usage: result.usage,
|
|
388
200
|
});
|
|
389
201
|
const responseMessage = result.choices?.[0]?.message;
|
|
390
|
-
|
|
202
|
+
// A valid completion response always carries content (possibly
|
|
203
|
+
// empty) on the message; protect against that specific case so
|
|
204
|
+
// downstream parsers see a string.
|
|
205
|
+
const content = responseMessage?.content ? responseMessage.content : "";
|
|
391
206
|
|
|
392
207
|
await this.#hooks.run.progress.emit({
|
|
393
208
|
projectId,
|
|
@@ -397,19 +212,38 @@ export default class TurnExecutor {
|
|
|
397
212
|
});
|
|
398
213
|
|
|
399
214
|
// Parse and emit — plugins handle audit storage
|
|
400
|
-
const { commands, unparsed } = XmlParser.parse(content);
|
|
215
|
+
const { commands, warnings, unparsed } = XmlParser.parse(content);
|
|
216
|
+
for (const w of warnings) {
|
|
217
|
+
await this.#hooks.error.log.emit({
|
|
218
|
+
store: this.#entries,
|
|
219
|
+
runId: currentRunId,
|
|
220
|
+
turn,
|
|
221
|
+
message: w,
|
|
222
|
+
loopId: currentLoopId,
|
|
223
|
+
status: 422,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (commands.length === 0 && !!unparsed?.trim() && warnings.length === 0) {
|
|
227
|
+
await this.#hooks.error.log.emit({
|
|
228
|
+
store: this.#entries,
|
|
229
|
+
runId: currentRunId,
|
|
230
|
+
turn,
|
|
231
|
+
loopId: currentLoopId,
|
|
232
|
+
message: "Response contained no actionable tags.",
|
|
233
|
+
status: 422,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
401
236
|
|
|
402
|
-
//
|
|
237
|
+
// Merge reasoning contributions from subscribers (think plugin's
|
|
238
|
+
// <think> tag, other plugin reasoning sources). Filter starts with
|
|
239
|
+
// the API-provided reasoning_content and layers on each plugin's
|
|
240
|
+
// contribution.
|
|
403
241
|
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;
|
|
242
|
+
const seed = responseMessage.reasoning_content
|
|
243
|
+
? responseMessage.reasoning_content
|
|
244
|
+
: "";
|
|
245
|
+
const merged = await this.#hooks.llm.reasoning.filter(seed, { commands });
|
|
246
|
+
responseMessage.reasoning_content = merged ? merged : null;
|
|
413
247
|
}
|
|
414
248
|
|
|
415
249
|
const systemMsg = filteredMessages.find((m) => m.role === "system");
|
|
@@ -445,171 +279,126 @@ export default class TurnExecutor {
|
|
|
445
279
|
// Sequential queue. Each tool completes before the next starts.
|
|
446
280
|
// On failure: abort remaining. On proposal: notify client, await
|
|
447
281
|
// resolution, continue.
|
|
448
|
-
|
|
282
|
+
// Narration text outside tags is fine when the turn also emitted
|
|
283
|
+
// at least one command — "OK", "Let me check:", reasoning prefixes
|
|
284
|
+
// are natural. Parse warnings and no-tags responses already emitted
|
|
285
|
+
// errors above; dispatch crashes and failed entries emit below.
|
|
449
286
|
let abortAfter = null;
|
|
450
287
|
|
|
451
288
|
for (const entry of recorded) {
|
|
289
|
+
if (entry.state === "failed" || entry.state === "cancelled") continue;
|
|
290
|
+
|
|
452
291
|
if (abortAfter) {
|
|
453
292
|
const errorMsg = `Aborted — preceding <${abortAfter}> failed.`;
|
|
454
|
-
await this.#
|
|
455
|
-
currentRunId,
|
|
293
|
+
await this.#entries.set({
|
|
294
|
+
runId: currentRunId,
|
|
456
295
|
turn,
|
|
457
|
-
entry.resultPath || entry.path,
|
|
458
|
-
errorMsg,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
296
|
+
path: entry.resultPath || entry.path,
|
|
297
|
+
body: errorMsg,
|
|
298
|
+
state: "failed",
|
|
299
|
+
outcome: "aborted",
|
|
300
|
+
attributes: { error: errorMsg },
|
|
301
|
+
loopId: currentLoopId,
|
|
302
|
+
});
|
|
463
303
|
continue;
|
|
464
304
|
}
|
|
465
305
|
|
|
466
306
|
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
467
|
-
|
|
307
|
+
try {
|
|
308
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
309
|
+
} catch (dispatchErr) {
|
|
310
|
+
await this.#hooks.error.log.emit({
|
|
311
|
+
store: this.#entries,
|
|
312
|
+
runId: currentRunId,
|
|
313
|
+
turn,
|
|
314
|
+
loopId: currentLoopId,
|
|
315
|
+
message: `Dispatch crash in ${entry.scheme}: ${dispatchErr.message}`,
|
|
316
|
+
status: 500,
|
|
317
|
+
});
|
|
318
|
+
abortAfter = entry.scheme;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
468
321
|
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
469
322
|
await this.#hooks.entry.created.emit(entry);
|
|
470
323
|
|
|
471
|
-
//
|
|
472
|
-
|
|
324
|
+
// Plugins (e.g. set) materialize pending proposals from the
|
|
325
|
+
// recorded entry — e.g. search/replace revisions → set:// 202.
|
|
326
|
+
await this.#hooks.proposal.prepare.emit({ rummy, recorded: [entry] });
|
|
473
327
|
|
|
474
328
|
// Check for any proposals created by this entry's dispatch
|
|
475
|
-
const proposed = await this.#
|
|
329
|
+
const proposed = await this.#entries.getUnresolved(currentRunId);
|
|
476
330
|
for (const p of proposed) {
|
|
477
|
-
await this.#hooks.
|
|
331
|
+
await this.#hooks.proposal.pending.emit({
|
|
478
332
|
projectId,
|
|
479
333
|
run: currentAlias,
|
|
480
334
|
proposed: [p],
|
|
481
335
|
});
|
|
482
|
-
await this.#
|
|
483
|
-
const resolved = await this.#
|
|
484
|
-
run_id: currentRunId,
|
|
485
|
-
path: p.path,
|
|
486
|
-
});
|
|
336
|
+
await this.#entries.waitForResolution(currentRunId, p.path);
|
|
337
|
+
const resolved = await this.#entries.getState(currentRunId, p.path);
|
|
487
338
|
if (resolved?.status >= 400) {
|
|
488
|
-
|
|
339
|
+
await this.#hooks.error.log.emit({
|
|
340
|
+
store: this.#entries,
|
|
341
|
+
runId: currentRunId,
|
|
342
|
+
turn,
|
|
343
|
+
loopId: currentLoopId,
|
|
344
|
+
message: `Proposal ${p.path} rejected: status ${resolved.status}.`,
|
|
345
|
+
status: resolved.status,
|
|
346
|
+
});
|
|
489
347
|
abortAfter = entry.scheme;
|
|
490
348
|
}
|
|
491
349
|
}
|
|
492
350
|
|
|
493
|
-
|
|
494
|
-
if (!hasErrors) {
|
|
351
|
+
if (!abortAfter) {
|
|
495
352
|
const entryPath = entry.resultPath || entry.path;
|
|
496
|
-
const row = await this.#
|
|
497
|
-
run_id: currentRunId,
|
|
498
|
-
path: entryPath,
|
|
499
|
-
});
|
|
353
|
+
const row = await this.#entries.getState(currentRunId, entryPath);
|
|
500
354
|
if (row?.status >= 400) {
|
|
501
|
-
|
|
355
|
+
await this.#hooks.error.log.emit({
|
|
356
|
+
store: this.#entries,
|
|
357
|
+
runId: currentRunId,
|
|
358
|
+
turn,
|
|
359
|
+
loopId: currentLoopId,
|
|
360
|
+
message: `Entry ${entryPath} failed: status ${row.status}.`,
|
|
361
|
+
status: row.status,
|
|
362
|
+
});
|
|
502
363
|
abortAfter = entry.scheme;
|
|
503
364
|
}
|
|
504
365
|
}
|
|
505
366
|
}
|
|
506
367
|
|
|
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");
|
|
368
|
+
// Turn Demotion: budget plugin re-materializes end-of-turn context
|
|
369
|
+
// and demotes this turn's promoted entries on overflow. Overflow
|
|
370
|
+
// emits an error (status 413) via the unified error channel.
|
|
371
|
+
await this.#hooks.budget.postDispatch({
|
|
372
|
+
contextSize,
|
|
373
|
+
ctx: budgetCtx,
|
|
374
|
+
rummy,
|
|
375
|
+
});
|
|
587
376
|
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
377
|
+
const { summaryText, updateText } = await this.#hooks.update.resolve({
|
|
378
|
+
recorded,
|
|
379
|
+
content,
|
|
380
|
+
commands,
|
|
381
|
+
runId: currentRunId,
|
|
382
|
+
turn,
|
|
383
|
+
loopId: currentLoopId,
|
|
384
|
+
rummy,
|
|
385
|
+
});
|
|
592
386
|
|
|
593
387
|
const askUserEntry = recorded.find((e) => e.scheme === "ask_user");
|
|
594
388
|
|
|
595
389
|
const turnResult = {
|
|
596
390
|
turn,
|
|
597
391
|
turnId: turnRow.id,
|
|
598
|
-
|
|
599
|
-
writeCalls,
|
|
600
|
-
unknownCalls,
|
|
392
|
+
recorded,
|
|
601
393
|
summaryText,
|
|
602
394
|
updateText,
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
flags,
|
|
606
|
-
model: result.model || requestedModel,
|
|
395
|
+
askUserCmd: askUserEntry,
|
|
396
|
+
model: result.model ? result.model : requestedModel,
|
|
607
397
|
modelAlias: requestedModel,
|
|
608
398
|
temperature: options?.temperature,
|
|
609
399
|
contextSize,
|
|
610
400
|
assembledTokens,
|
|
611
401
|
usage: result.usage,
|
|
612
|
-
budgetRecovery,
|
|
613
402
|
};
|
|
614
403
|
|
|
615
404
|
await this.#hooks.turn.completed.emit(turnResult);
|
|
@@ -623,59 +412,78 @@ export default class TurnExecutor {
|
|
|
623
412
|
*/
|
|
624
413
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
625
414
|
const scheme = cmd.name;
|
|
626
|
-
|
|
415
|
+
// Each tool's XmlParser shape surfaces exactly one of these
|
|
416
|
+
// three fields as its addressable target. Treat absent as empty
|
|
417
|
+
// so the length/control-char validation below catches bad shapes
|
|
418
|
+
// rather than letting an undefined slip through.
|
|
419
|
+
let rawTarget = "";
|
|
420
|
+
if (cmd.path) rawTarget = cmd.path;
|
|
421
|
+
else if (cmd.command) rawTarget = cmd.command;
|
|
422
|
+
else if (cmd.question) rawTarget = cmd.question;
|
|
627
423
|
// Reject paths that are likely reasoning bleed — too long or contain non-printing chars
|
|
628
424
|
if (rawTarget.length > 512 || /\p{Cc}/u.test(rawTarget)) {
|
|
629
|
-
const rejectPath = await this.#
|
|
425
|
+
const rejectPath = await this.#entries.logPath(
|
|
630
426
|
runId,
|
|
631
|
-
scheme,
|
|
632
|
-
`${scheme}://invalid`,
|
|
633
427
|
turn,
|
|
428
|
+
scheme,
|
|
429
|
+
"invalid",
|
|
634
430
|
);
|
|
635
|
-
await this.#
|
|
431
|
+
await this.#entries.set({
|
|
636
432
|
runId,
|
|
637
433
|
turn,
|
|
638
|
-
rejectPath,
|
|
639
|
-
`Invalid path: too long or contains non-printing characters`,
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
434
|
+
path: rejectPath,
|
|
435
|
+
body: `Invalid path: too long or contains non-printing characters`,
|
|
436
|
+
state: "failed",
|
|
437
|
+
outcome: "validation",
|
|
438
|
+
attributes: { action: scheme },
|
|
439
|
+
loopId,
|
|
440
|
+
});
|
|
643
441
|
return {
|
|
644
442
|
scheme,
|
|
645
443
|
path: rejectPath,
|
|
646
444
|
body: "",
|
|
647
445
|
attributes: {},
|
|
648
|
-
|
|
446
|
+
state: "failed",
|
|
447
|
+
outcome: "validation",
|
|
649
448
|
resultPath: rejectPath,
|
|
650
449
|
};
|
|
651
450
|
}
|
|
652
451
|
const target = rawTarget;
|
|
653
|
-
const resultPath = await this.#
|
|
654
|
-
runId,
|
|
655
|
-
scheme,
|
|
656
|
-
target,
|
|
657
|
-
turn,
|
|
658
|
-
);
|
|
452
|
+
const resultPath = await this.#entries.logPath(runId, turn, scheme, target);
|
|
659
453
|
|
|
660
454
|
// Pass parsed command fields through as attributes
|
|
661
455
|
const { name: _, ...attributes } = cmd;
|
|
662
456
|
if (cmd.path) attributes.path = target;
|
|
663
457
|
|
|
664
|
-
|
|
458
|
+
// Same per-shape resolution as rawTarget; the three sources are
|
|
459
|
+
// mutually exclusive per tool. Empty string when none set.
|
|
460
|
+
let body = "";
|
|
461
|
+
if (cmd.body) body = cmd.body;
|
|
462
|
+
else if (cmd.command) body = cmd.command;
|
|
463
|
+
else if (cmd.question) body = cmd.question;
|
|
665
464
|
|
|
666
465
|
// Filter: plugins can validate/transform before recording
|
|
667
466
|
const filtered = await this.#hooks.entry.recording.filter(
|
|
668
|
-
{
|
|
669
|
-
|
|
467
|
+
{
|
|
468
|
+
scheme,
|
|
469
|
+
path: resultPath,
|
|
470
|
+
body,
|
|
471
|
+
attributes,
|
|
472
|
+
state: "resolved",
|
|
473
|
+
outcome: null,
|
|
474
|
+
},
|
|
475
|
+
{ store: this.#entries, runId, turn, loopId, mode },
|
|
670
476
|
);
|
|
671
|
-
if (filtered.
|
|
477
|
+
if (filtered.state === "failed" || filtered.state === "cancelled") {
|
|
478
|
+
return filtered;
|
|
479
|
+
}
|
|
672
480
|
|
|
673
481
|
return {
|
|
674
482
|
scheme: filtered.scheme,
|
|
675
483
|
path: filtered.path,
|
|
676
484
|
body: filtered.body,
|
|
677
485
|
attributes: filtered.attributes,
|
|
678
|
-
|
|
486
|
+
state: "resolved",
|
|
679
487
|
resultPath: filtered.path,
|
|
680
488
|
};
|
|
681
489
|
}
|