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