@possumtech/rummy 0.2.8 → 0.3.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 +13 -2
- package/EXCEPTIONS.md +46 -0
- package/PLUGINS.md +422 -188
- package/SPEC.md +440 -106
- package/migrations/001_initial_schema.sql +5 -3
- package/package.json +17 -5
- package/service.js +5 -3
- package/src/agent/AgentLoop.js +252 -55
- package/src/agent/ContextAssembler.js +20 -4
- package/src/agent/KnownStore.js +82 -25
- package/src/agent/ProjectAgent.js +4 -1
- package/src/agent/ResponseHealer.js +86 -32
- package/src/agent/TurnExecutor.js +542 -207
- package/src/agent/XmlParser.js +77 -41
- package/src/agent/known_store.sql +68 -4
- package/src/agent/schemes.sql +3 -0
- package/src/agent/tokens.js +7 -21
- package/src/agent/turns.sql +15 -1
- package/src/hooks/HookRegistry.js +7 -0
- package/src/hooks/Hooks.js +15 -0
- package/src/hooks/PluginContext.js +14 -1
- package/src/hooks/RummyContext.js +16 -4
- package/src/hooks/ToolRegistry.js +77 -19
- package/src/llm/LlmProvider.js +27 -8
- package/src/llm/OpenAiClient.js +20 -0
- package/src/llm/OpenRouterClient.js +24 -2
- package/src/llm/XaiClient.js +47 -2
- package/src/plugins/ask_user/README.md +4 -4
- package/src/plugins/ask_user/ask_user.js +5 -5
- package/src/plugins/ask_user/ask_userDoc.js +29 -0
- package/src/plugins/budget/README.md +31 -0
- package/src/plugins/budget/budget.js +55 -0
- package/src/plugins/cp/README.md +5 -4
- package/src/plugins/cp/cp.js +10 -6
- package/src/plugins/cp/cpDoc.js +29 -0
- package/src/plugins/engine/engine.sql +1 -8
- package/src/plugins/engine/turn_context.sql +4 -9
- package/src/plugins/env/README.md +3 -4
- package/src/plugins/env/env.js +5 -5
- package/src/plugins/env/envDoc.js +29 -0
- package/src/plugins/file/README.md +9 -12
- package/src/plugins/file/file.js +34 -35
- package/src/plugins/get/README.md +2 -2
- package/src/plugins/get/get.js +77 -6
- package/src/plugins/get/getDoc.js +51 -0
- package/src/plugins/hedberg/hedberg.js +2 -1
- package/src/plugins/hedberg/matcher.js +10 -29
- package/src/plugins/hedberg/normalize.js +28 -0
- package/src/plugins/hedberg/patterns.js +25 -27
- package/src/plugins/hedberg/sed.js +17 -10
- package/src/plugins/index.js +66 -14
- package/src/plugins/instructions/README.md +6 -2
- package/src/plugins/instructions/instructions.js +20 -4
- package/src/plugins/instructions/preamble.md +19 -5
- package/src/plugins/known/README.md +10 -7
- package/src/plugins/known/known.js +23 -17
- package/src/plugins/known/knownDoc.js +34 -0
- package/src/plugins/mv/README.md +5 -4
- package/src/plugins/mv/mv.js +27 -6
- package/src/plugins/mv/mvDoc.js +45 -0
- package/src/plugins/performed/README.md +15 -0
- package/src/plugins/performed/performed.js +45 -0
- package/src/plugins/persona/persona.js +78 -0
- package/src/plugins/previous/README.md +3 -2
- package/src/plugins/previous/previous.js +33 -24
- package/src/plugins/progress/README.md +1 -2
- package/src/plugins/progress/progress.js +33 -21
- package/src/plugins/prompt/README.md +5 -5
- package/src/plugins/prompt/prompt.js +15 -17
- package/src/plugins/rm/README.md +4 -4
- package/src/plugins/rm/rm.js +32 -20
- package/src/plugins/rm/rmDoc.js +30 -0
- package/src/plugins/rpc/README.md +15 -28
- package/src/plugins/rpc/rpc.js +42 -77
- package/src/plugins/set/README.md +13 -12
- package/src/plugins/set/set.js +107 -16
- package/src/plugins/set/setDoc.js +49 -0
- package/src/plugins/sh/README.md +4 -4
- package/src/plugins/sh/sh.js +5 -5
- package/src/plugins/sh/shDoc.js +29 -0
- package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
- package/src/plugins/summarize/README.md +6 -5
- package/src/plugins/summarize/summarize.js +7 -6
- package/src/plugins/summarize/summarizeDoc.js +33 -0
- package/src/plugins/telemetry/telemetry.js +16 -9
- package/src/plugins/think/README.md +20 -0
- package/src/plugins/think/think.js +5 -0
- package/src/plugins/unknown/README.md +6 -5
- package/src/plugins/unknown/unknown.js +12 -9
- package/src/plugins/unknown/unknownDoc.js +31 -0
- package/src/plugins/update/README.md +3 -8
- package/src/plugins/update/update.js +7 -6
- package/src/plugins/update/updateDoc.js +33 -0
- package/src/server/ClientConnection.js +59 -45
- package/src/server/RpcRegistry.js +52 -4
- package/src/sql/v_model_context.sql +10 -25
- package/src/plugins/ask_user/docs.md +0 -2
- package/src/plugins/cp/docs.md +0 -2
- package/src/plugins/current/README.md +0 -14
- package/src/plugins/current/current.js +0 -47
- package/src/plugins/env/docs.md +0 -4
- package/src/plugins/get/docs.md +0 -10
- package/src/plugins/known/docs.md +0 -3
- package/src/plugins/mv/docs.md +0 -2
- package/src/plugins/rm/docs.md +0 -6
- package/src/plugins/set/docs.md +0 -6
- package/src/plugins/sh/docs.md +0 -2
- package/src/plugins/skills/README.md +0 -25
- package/src/plugins/store/README.md +0 -20
- package/src/plugins/store/docs.md +0 -6
- package/src/plugins/store/store.js +0 -63
- package/src/plugins/summarize/docs.md +0 -4
- package/src/plugins/unknown/docs.md +0 -5
- package/src/plugins/update/docs.md +0 -4
|
@@ -19,6 +19,68 @@ export default class TurnExecutor {
|
|
|
19
19
|
this.#knownStore = knownStore;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Rebuild turn_context from v_model_context, then assemble messages.
|
|
24
|
+
* Called at turn start and again after any fidelity demotion within the turn.
|
|
25
|
+
*/
|
|
26
|
+
async #materializeTurnContext({
|
|
27
|
+
runId,
|
|
28
|
+
loopId,
|
|
29
|
+
turn,
|
|
30
|
+
systemPrompt,
|
|
31
|
+
mode,
|
|
32
|
+
toolSet,
|
|
33
|
+
contextSize,
|
|
34
|
+
demoted,
|
|
35
|
+
}) {
|
|
36
|
+
await this.#db.clear_turn_context.run({ run_id: runId, turn });
|
|
37
|
+
const viewRows = await this.#db.get_model_context.all({ run_id: runId });
|
|
38
|
+
for (const row of viewRows) {
|
|
39
|
+
const scheme = row.scheme || "file";
|
|
40
|
+
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
41
|
+
path: row.path,
|
|
42
|
+
scheme,
|
|
43
|
+
body: row.body,
|
|
44
|
+
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
45
|
+
fidelity: row.fidelity,
|
|
46
|
+
category: row.category,
|
|
47
|
+
});
|
|
48
|
+
await this.#db.insert_turn_context.run({
|
|
49
|
+
run_id: runId,
|
|
50
|
+
loop_id: loopId,
|
|
51
|
+
turn,
|
|
52
|
+
ordinal: row.ordinal,
|
|
53
|
+
path: row.path,
|
|
54
|
+
fidelity: row.fidelity,
|
|
55
|
+
status: row.status,
|
|
56
|
+
body: projectedBody ?? "",
|
|
57
|
+
tokens: countTokens(projectedBody ?? ""),
|
|
58
|
+
attributes: row.attributes,
|
|
59
|
+
category: row.category,
|
|
60
|
+
source_turn: row.turn,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const rows = await this.#db.get_turn_context.all({ run_id: runId, turn });
|
|
64
|
+
const lastCtx = await this.#db.get_last_context_tokens.get({
|
|
65
|
+
run_id: runId,
|
|
66
|
+
});
|
|
67
|
+
const lastContextTokens = lastCtx?.context_tokens ?? 0;
|
|
68
|
+
const messages = await ContextAssembler.assembleFromTurnContext(
|
|
69
|
+
rows,
|
|
70
|
+
{
|
|
71
|
+
type: mode,
|
|
72
|
+
systemPrompt,
|
|
73
|
+
contextSize,
|
|
74
|
+
demoted,
|
|
75
|
+
toolSet,
|
|
76
|
+
lastContextTokens,
|
|
77
|
+
turn,
|
|
78
|
+
},
|
|
79
|
+
this.#hooks,
|
|
80
|
+
);
|
|
81
|
+
return { rows, messages, lastContextTokens };
|
|
82
|
+
}
|
|
83
|
+
|
|
22
84
|
async execute({
|
|
23
85
|
mode,
|
|
24
86
|
project,
|
|
@@ -28,11 +90,25 @@ export default class TurnExecutor {
|
|
|
28
90
|
currentLoopId,
|
|
29
91
|
requestedModel,
|
|
30
92
|
loopPrompt,
|
|
31
|
-
|
|
93
|
+
loopIteration,
|
|
94
|
+
noRepo,
|
|
95
|
+
toolSet,
|
|
96
|
+
inRecovery = false,
|
|
32
97
|
contextSize,
|
|
33
98
|
options,
|
|
34
99
|
signal,
|
|
35
100
|
}) {
|
|
101
|
+
const RECOVERY_EXCLUDED = new Set([
|
|
102
|
+
"sh",
|
|
103
|
+
"env",
|
|
104
|
+
"search",
|
|
105
|
+
"ask_user",
|
|
106
|
+
"set",
|
|
107
|
+
]);
|
|
108
|
+
const effectiveToolSet = inRecovery
|
|
109
|
+
? new Set([...toolSet].filter((t) => !RECOVERY_EXCLUDED.has(t)))
|
|
110
|
+
: toolSet;
|
|
111
|
+
|
|
36
112
|
const turn = await this.#knownStore.nextTurn(currentRunId);
|
|
37
113
|
|
|
38
114
|
const turnRow = await this.#db.create_turn.get({
|
|
@@ -71,7 +147,8 @@ export default class TurnExecutor {
|
|
|
71
147
|
runId: currentRunId,
|
|
72
148
|
loopId: currentLoopId,
|
|
73
149
|
turnId: turnRow.id,
|
|
74
|
-
|
|
150
|
+
noRepo,
|
|
151
|
+
toolSet: effectiveToolSet,
|
|
75
152
|
contextSize,
|
|
76
153
|
systemPrompt: null,
|
|
77
154
|
loopPrompt,
|
|
@@ -83,6 +160,7 @@ export default class TurnExecutor {
|
|
|
83
160
|
mode,
|
|
84
161
|
prompt: loopPrompt,
|
|
85
162
|
isContinuation: options?.isContinuation,
|
|
163
|
+
loopIteration,
|
|
86
164
|
});
|
|
87
165
|
|
|
88
166
|
await this.#hooks.processTurn(rummy);
|
|
@@ -109,38 +187,24 @@ export default class TurnExecutor {
|
|
|
109
187
|
});
|
|
110
188
|
|
|
111
189
|
// Materialize turn_context: VIEW rows projected through tools
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const scheme = row.scheme || "file";
|
|
118
|
-
const projectedBody = await this.#hooks.tools.view(scheme, {
|
|
119
|
-
path: row.path,
|
|
120
|
-
scheme,
|
|
121
|
-
body: row.body,
|
|
122
|
-
attributes: row.attributes ? JSON.parse(row.attributes) : null,
|
|
123
|
-
fidelity: row.fidelity,
|
|
124
|
-
category: row.category,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
await this.#db.insert_turn_context.run({
|
|
128
|
-
run_id: currentRunId,
|
|
129
|
-
loop_id: currentLoopId,
|
|
190
|
+
const demoted = [];
|
|
191
|
+
let { rows, messages, lastContextTokens } =
|
|
192
|
+
await this.#materializeTurnContext({
|
|
193
|
+
runId: currentRunId,
|
|
194
|
+
loopId: currentLoopId,
|
|
130
195
|
turn,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
tokens: countTokens(projectedBody ?? ""),
|
|
137
|
-
attributes: row.attributes,
|
|
138
|
-
category: row.category,
|
|
139
|
-
source_turn: row.turn,
|
|
196
|
+
systemPrompt,
|
|
197
|
+
mode,
|
|
198
|
+
toolSet: effectiveToolSet,
|
|
199
|
+
contextSize,
|
|
200
|
+
demoted,
|
|
140
201
|
});
|
|
141
|
-
}
|
|
142
202
|
|
|
143
|
-
|
|
203
|
+
await this.#hooks.context.materialized.emit({
|
|
204
|
+
runId: currentRunId,
|
|
205
|
+
turn,
|
|
206
|
+
rowCount: rows.length,
|
|
207
|
+
});
|
|
144
208
|
|
|
145
209
|
await this.#hooks.run.progress.emit({
|
|
146
210
|
projectId,
|
|
@@ -149,94 +213,74 @@ export default class TurnExecutor {
|
|
|
149
213
|
status: "thinking",
|
|
150
214
|
});
|
|
151
215
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
turn,
|
|
156
|
-
});
|
|
157
|
-
let messages = await ContextAssembler.assembleFromTurnContext(
|
|
216
|
+
const budgetResult = await this.#hooks.budget.enforce({
|
|
217
|
+
contextSize,
|
|
218
|
+
messages,
|
|
158
219
|
rows,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const ceiling = contextSize * 0.95;
|
|
175
|
-
if (assembledTokens > ceiling) {
|
|
176
|
-
const candidates = rows
|
|
177
|
-
.filter(
|
|
178
|
-
(r) =>
|
|
179
|
-
r.fidelity === "full" &&
|
|
180
|
-
r.tokens > 0 &&
|
|
181
|
-
(r.category === "file" || r.category === "known"),
|
|
182
|
-
)
|
|
183
|
-
.toSorted((a, b) => a.source_turn - b.source_turn);
|
|
184
|
-
|
|
185
|
-
let excess = assembledTokens - ceiling;
|
|
186
|
-
for (const entry of candidates) {
|
|
187
|
-
if (excess <= 0) break;
|
|
220
|
+
lastPromptTokens: lastContextTokens,
|
|
221
|
+
});
|
|
222
|
+
messages = budgetResult.messages;
|
|
223
|
+
rows = budgetResult.rows;
|
|
224
|
+
let assembledTokens =
|
|
225
|
+
budgetResult.assembledTokens ??
|
|
226
|
+
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
227
|
+
|
|
228
|
+
if (budgetResult.status === 413) {
|
|
229
|
+
if (loopIteration === 1) {
|
|
230
|
+
// Prompt Demotion: first-turn overflow — demote incoming prompt to summary
|
|
231
|
+
const promptRow = rows.findLast(
|
|
232
|
+
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
233
|
+
);
|
|
234
|
+
if (promptRow) {
|
|
188
235
|
await this.#knownStore.setFidelity(
|
|
189
236
|
currentRunId,
|
|
190
|
-
|
|
237
|
+
promptRow.path,
|
|
191
238
|
"summary",
|
|
192
239
|
);
|
|
193
|
-
excess -= entry.tokens;
|
|
194
|
-
demoted.push(entry.path);
|
|
195
240
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
tokens: countTokens(projectedBody ?? ""),
|
|
222
|
-
attributes: row.attributes,
|
|
223
|
-
category: row.category,
|
|
224
|
-
source_turn: row.turn,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
rows = await this.#db.get_turn_context.all({
|
|
228
|
-
run_id: currentRunId,
|
|
241
|
+
const reMat = await this.#materializeTurnContext({
|
|
242
|
+
runId: currentRunId,
|
|
243
|
+
loopId: currentLoopId,
|
|
244
|
+
turn,
|
|
245
|
+
systemPrompt,
|
|
246
|
+
mode,
|
|
247
|
+
toolSet: effectiveToolSet,
|
|
248
|
+
contextSize,
|
|
249
|
+
demoted,
|
|
250
|
+
});
|
|
251
|
+
rows = reMat.rows;
|
|
252
|
+
messages = reMat.messages;
|
|
253
|
+
const recheck = await this.#hooks.budget.enforce({
|
|
254
|
+
contextSize,
|
|
255
|
+
messages,
|
|
256
|
+
rows,
|
|
257
|
+
lastPromptTokens: reMat.lastContextTokens,
|
|
258
|
+
});
|
|
259
|
+
messages = recheck.messages;
|
|
260
|
+
rows = recheck.rows;
|
|
261
|
+
assembledTokens =
|
|
262
|
+
recheck.assembledTokens ??
|
|
263
|
+
messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
264
|
+
if (recheck.status === 413) {
|
|
265
|
+
return {
|
|
229
266
|
turn,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
console.warn(
|
|
237
|
-
`[RUMMY] Budget exceeded: demoted ${demoted.length} entries to fit ${contextSize} token limit`,
|
|
238
|
-
);
|
|
267
|
+
turnId: turnRow.id,
|
|
268
|
+
status: 413,
|
|
269
|
+
assembledTokens,
|
|
270
|
+
contextSize,
|
|
271
|
+
overflow: recheck.overflow,
|
|
272
|
+
};
|
|
239
273
|
}
|
|
274
|
+
} else {
|
|
275
|
+
// Base context too large even without new prompt — genuine failure
|
|
276
|
+
return {
|
|
277
|
+
turn,
|
|
278
|
+
turnId: turnRow.id,
|
|
279
|
+
status: 413,
|
|
280
|
+
assembledTokens,
|
|
281
|
+
contextSize,
|
|
282
|
+
overflow: budgetResult.overflow,
|
|
283
|
+
};
|
|
240
284
|
}
|
|
241
285
|
}
|
|
242
286
|
|
|
@@ -246,14 +290,34 @@ export default class TurnExecutor {
|
|
|
246
290
|
runId: currentRunId,
|
|
247
291
|
});
|
|
248
292
|
|
|
249
|
-
// Store assembled messages as audit
|
|
250
293
|
// Call LLM
|
|
251
294
|
await this.#hooks.llm.request.started.emit({ model: requestedModel, turn });
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
295
|
+
let rawResult;
|
|
296
|
+
const isTransient = (e) =>
|
|
297
|
+
/\b(503|429|timeout|ECONNREFUSED|ECONNRESET|unavailable)\b/i.test(
|
|
298
|
+
e.message,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
for (let llmAttempt = 0; ; llmAttempt++) {
|
|
302
|
+
try {
|
|
303
|
+
rawResult = await this.#llmProvider.completion(
|
|
304
|
+
filteredMessages,
|
|
305
|
+
requestedModel,
|
|
306
|
+
{ temperature: options?.temperature, signal },
|
|
307
|
+
);
|
|
308
|
+
break;
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (isTransient(err) && llmAttempt < 3) {
|
|
311
|
+
const delay = 1000 * 2 ** llmAttempt;
|
|
312
|
+
console.warn(
|
|
313
|
+
`[RUMMY] Transient LLM error (attempt ${llmAttempt + 1}/3): ${err.message.slice(0, 120)}. Retrying in ${delay}ms.`,
|
|
314
|
+
);
|
|
315
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
257
321
|
const result = await this.#hooks.llm.response.filter(rawResult, {
|
|
258
322
|
model: requestedModel,
|
|
259
323
|
projectId,
|
|
@@ -277,6 +341,19 @@ export default class TurnExecutor {
|
|
|
277
341
|
// Parse and emit — plugins handle audit storage
|
|
278
342
|
const { commands, unparsed } = XmlParser.parse(content);
|
|
279
343
|
|
|
344
|
+
// Ensure reasoning_content captures both API field and <think> tag
|
|
345
|
+
if (responseMessage) {
|
|
346
|
+
const thinkCmds = commands.filter((c) => c.name === "think");
|
|
347
|
+
const thinkText = thinkCmds
|
|
348
|
+
.map((c) => c.body)
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
.join("\n");
|
|
351
|
+
const apiReasoning = responseMessage.reasoning_content || "";
|
|
352
|
+
const parts = [apiReasoning, thinkText].filter(Boolean);
|
|
353
|
+
responseMessage.reasoning_content =
|
|
354
|
+
parts.length > 0 ? parts.join("\n") : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
280
357
|
const systemMsg = filteredMessages.find((m) => m.role === "system");
|
|
281
358
|
const userMsg = filteredMessages.find((m) => m.role === "user");
|
|
282
359
|
await this.#hooks.turn.response.emit({
|
|
@@ -287,16 +364,21 @@ export default class TurnExecutor {
|
|
|
287
364
|
content,
|
|
288
365
|
commands,
|
|
289
366
|
unparsed,
|
|
367
|
+
assembledTokens,
|
|
368
|
+
contextSize,
|
|
290
369
|
systemMsg: systemMsg?.content,
|
|
291
370
|
userMsg: userMsg?.content,
|
|
292
371
|
});
|
|
293
372
|
|
|
294
373
|
// --- PHASE 1: RECORD ---
|
|
295
|
-
//
|
|
374
|
+
// Split lifecycle signals from action commands.
|
|
375
|
+
// Lifecycle signals (summarize, update, unknown, known) are state
|
|
376
|
+
// declarations — always recorded, never 409'd by sequential dispatch.
|
|
377
|
+
const LIFECYCLE = new Set(["summarize", "update", "unknown", "known"]);
|
|
296
378
|
|
|
297
379
|
const recorded = [];
|
|
298
|
-
|
|
299
|
-
|
|
380
|
+
const lifecycle = [];
|
|
381
|
+
const actions = [];
|
|
300
382
|
|
|
301
383
|
for (const cmd of commands) {
|
|
302
384
|
const entry = await this.#record(
|
|
@@ -307,94 +389,217 @@ export default class TurnExecutor {
|
|
|
307
389
|
cmd,
|
|
308
390
|
);
|
|
309
391
|
if (!entry) continue;
|
|
392
|
+
recorded.push(entry);
|
|
310
393
|
|
|
311
|
-
if (entry.scheme
|
|
312
|
-
|
|
313
|
-
else
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// If model sent both, summary wins
|
|
317
|
-
if (summaryText && updateText) updateText = null;
|
|
318
|
-
|
|
319
|
-
// If model sent neither, heal from content
|
|
320
|
-
let statusHealed = false;
|
|
321
|
-
if (!summaryText && !updateText) {
|
|
322
|
-
const healed = ResponseHealer.healStatus(content, commands);
|
|
323
|
-
summaryText = healed.summaryText;
|
|
324
|
-
updateText = healed.updateText;
|
|
325
|
-
statusHealed = true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Record healed status
|
|
329
|
-
if (summaryText) {
|
|
330
|
-
const summaryPath = await this.#knownStore.slugPath(
|
|
331
|
-
currentRunId,
|
|
332
|
-
"summarize",
|
|
333
|
-
summaryText,
|
|
334
|
-
);
|
|
335
|
-
await this.#knownStore.upsert(
|
|
336
|
-
currentRunId,
|
|
337
|
-
turn,
|
|
338
|
-
summaryPath,
|
|
339
|
-
summaryText,
|
|
340
|
-
200,
|
|
341
|
-
{ loopId: currentLoopId },
|
|
342
|
-
);
|
|
343
|
-
} else if (updateText) {
|
|
344
|
-
const updatePath = await this.#knownStore.slugPath(
|
|
345
|
-
currentRunId,
|
|
346
|
-
"update",
|
|
347
|
-
updateText,
|
|
348
|
-
);
|
|
349
|
-
await this.#knownStore.upsert(
|
|
350
|
-
currentRunId,
|
|
351
|
-
turn,
|
|
352
|
-
updatePath,
|
|
353
|
-
updateText,
|
|
354
|
-
200,
|
|
355
|
-
{ loopId: currentLoopId },
|
|
356
|
-
);
|
|
394
|
+
if (LIFECYCLE.has(entry.scheme)) {
|
|
395
|
+
lifecycle.push(entry);
|
|
396
|
+
} else {
|
|
397
|
+
actions.push(entry);
|
|
398
|
+
}
|
|
357
399
|
}
|
|
358
400
|
|
|
359
401
|
// --- PHASE 2: DISPATCH ---
|
|
360
|
-
|
|
402
|
+
let hasErrors = false;
|
|
403
|
+
let hasProposed = false;
|
|
404
|
+
let abortAfter = null;
|
|
405
|
+
const dispatched = [...lifecycle];
|
|
361
406
|
|
|
362
|
-
|
|
407
|
+
// Lifecycle signals first — always dispatched, never aborted.
|
|
408
|
+
for (const entry of lifecycle) {
|
|
409
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
363
410
|
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
411
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
364
412
|
await this.#hooks.entry.created.emit(entry);
|
|
365
413
|
}
|
|
366
414
|
|
|
367
|
-
|
|
368
|
-
|
|
415
|
+
for (const entry of actions) {
|
|
416
|
+
if (abortAfter) {
|
|
417
|
+
const errorMsg = `Aborted — preceding <${abortAfter}> requires resolution.`;
|
|
418
|
+
await this.#knownStore.upsert(
|
|
419
|
+
currentRunId,
|
|
420
|
+
turn,
|
|
421
|
+
entry.resultPath || entry.path,
|
|
422
|
+
errorMsg,
|
|
423
|
+
409,
|
|
424
|
+
{ attributes: { error: errorMsg }, loopId: currentLoopId },
|
|
425
|
+
);
|
|
426
|
+
hasErrors = true;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await this.#hooks.tool.before.emit({ entry, rummy });
|
|
431
|
+
await this.#hooks.tools.dispatch(entry.scheme, entry, rummy);
|
|
432
|
+
await this.#hooks.tool.after.emit({ entry, rummy });
|
|
433
|
+
await this.#hooks.entry.created.emit(entry);
|
|
434
|
+
dispatched.push(entry);
|
|
369
435
|
|
|
370
|
-
// Check if any dispatched entries ended in error or proposed state
|
|
371
|
-
let hasErrors = false;
|
|
372
|
-
let hasProposed = false;
|
|
373
|
-
for (const entry of recorded) {
|
|
374
436
|
const row = await this.#db.get_entry_state.get({
|
|
375
437
|
run_id: currentRunId,
|
|
376
438
|
path: entry.resultPath || entry.path,
|
|
377
439
|
});
|
|
378
|
-
if (row?.status
|
|
379
|
-
|
|
440
|
+
if (row?.status === 202) {
|
|
441
|
+
hasProposed = true;
|
|
442
|
+
abortAfter = entry.scheme;
|
|
443
|
+
} else if (row?.status >= 400) {
|
|
444
|
+
hasErrors = true;
|
|
445
|
+
abortAfter = entry.scheme;
|
|
446
|
+
}
|
|
380
447
|
}
|
|
381
448
|
|
|
382
|
-
//
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
// Materialize proposals only if we dispatched actions
|
|
450
|
+
if (!abortAfter || hasProposed) {
|
|
451
|
+
await this.#hooks.turn.proposing.emit({ rummy, recorded: dispatched });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Recheck after materialization (set handler may create proposals)
|
|
455
|
+
if (!hasProposed && !hasErrors) {
|
|
456
|
+
for (const entry of actions) {
|
|
457
|
+
const row = await this.#db.get_entry_state.get({
|
|
458
|
+
run_id: currentRunId,
|
|
459
|
+
path: entry.resultPath || entry.path,
|
|
460
|
+
});
|
|
461
|
+
if (row?.status === 202) hasProposed = true;
|
|
462
|
+
if (row?.status >= 400) hasErrors = true;
|
|
463
|
+
}
|
|
386
464
|
}
|
|
387
465
|
|
|
388
|
-
//
|
|
389
|
-
|
|
466
|
+
// Turn Demotion: if end-of-turn context exceeds ceiling, demote this
|
|
467
|
+
// turn's data entries and the incoming prompt to summary, then force a
|
|
468
|
+
// budget recovery phase before continuing.
|
|
469
|
+
let budgetRecovery = null;
|
|
470
|
+
// Use actual prompt_tokens from this turn's LLM response as the ground-truth
|
|
471
|
+
// token count for post-turn budget checks — more accurate than the estimate.
|
|
472
|
+
const currentPromptTokens = result.usage?.prompt_tokens ?? 0;
|
|
473
|
+
if (contextSize) {
|
|
474
|
+
const postMat = await this.#materializeTurnContext({
|
|
475
|
+
runId: currentRunId,
|
|
476
|
+
loopId: currentLoopId,
|
|
477
|
+
turn,
|
|
478
|
+
systemPrompt,
|
|
479
|
+
mode,
|
|
480
|
+
toolSet: effectiveToolSet,
|
|
481
|
+
contextSize,
|
|
482
|
+
demoted,
|
|
483
|
+
});
|
|
484
|
+
const postBudget = await this.#hooks.budget.enforce({
|
|
485
|
+
contextSize,
|
|
486
|
+
messages: postMat.messages,
|
|
487
|
+
rows: postMat.rows,
|
|
488
|
+
lastPromptTokens: currentPromptTokens,
|
|
489
|
+
});
|
|
490
|
+
if (postBudget.status === 413) {
|
|
491
|
+
// Demote this turn's data entries.
|
|
492
|
+
const demotedEntries = await this.#db.demote_turn_data_entries.all({
|
|
493
|
+
run_id: currentRunId,
|
|
494
|
+
turn,
|
|
495
|
+
});
|
|
496
|
+
const paths = demotedEntries.map((r) => r.path).join(", ");
|
|
497
|
+
|
|
498
|
+
// Also summarize the prompt — forces the model to earn it back.
|
|
499
|
+
const promptRow = postMat.rows.find((r) => r.scheme === "prompt");
|
|
500
|
+
if (promptRow) {
|
|
501
|
+
await this.#knownStore.setFidelity(
|
|
502
|
+
currentRunId,
|
|
503
|
+
promptRow.path,
|
|
504
|
+
"summary",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Re-materialize after both demotions for accurate token count.
|
|
509
|
+
const recoveryMat = await this.#materializeTurnContext({
|
|
510
|
+
runId: currentRunId,
|
|
511
|
+
loopId: currentLoopId,
|
|
512
|
+
turn,
|
|
513
|
+
systemPrompt,
|
|
514
|
+
mode,
|
|
515
|
+
toolSet: effectiveToolSet,
|
|
516
|
+
contextSize,
|
|
517
|
+
demoted,
|
|
518
|
+
});
|
|
519
|
+
const recoveryBudget = await this.#hooks.budget.enforce({
|
|
520
|
+
contextSize,
|
|
521
|
+
messages: recoveryMat.messages,
|
|
522
|
+
rows: recoveryMat.rows,
|
|
523
|
+
lastPromptTokens: currentPromptTokens,
|
|
524
|
+
});
|
|
525
|
+
const safeLevel = Math.floor(contextSize * 0.9);
|
|
526
|
+
const tokensToFree = Math.max(
|
|
527
|
+
0,
|
|
528
|
+
recoveryBudget.assembledTokens - safeLevel,
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const promptLine =
|
|
532
|
+
tokensToFree > 0
|
|
533
|
+
? `Info: Prompt auto-summarized. Full prompt restores automatically when you free ${tokensToFree} tokens.`
|
|
534
|
+
: "Info: Prompt auto-summarized. It will restore automatically.";
|
|
535
|
+
const body = [
|
|
536
|
+
"Error 413: Context Size Exceeded",
|
|
537
|
+
"",
|
|
538
|
+
"Required: YOU MUST demote larger and/or less relevant items to optimize your context.",
|
|
539
|
+
`Info: ${paths} have been automatically summarized to avoid overflow.`,
|
|
540
|
+
promptLine,
|
|
541
|
+
"Info: YOU MAY use bulk patterns to demote and promote entries by pattern.",
|
|
542
|
+
"Info: Well-designed paths and summaries improve context management.",
|
|
543
|
+
'Example: <set path="known://people/*" fidelity="summary"/>',
|
|
544
|
+
].join("\n");
|
|
545
|
+
|
|
546
|
+
await this.#knownStore.upsert(
|
|
547
|
+
currentRunId,
|
|
548
|
+
turn,
|
|
549
|
+
`budget://${currentLoopId}/${turn}`,
|
|
550
|
+
body,
|
|
551
|
+
413,
|
|
552
|
+
{ loopId: currentLoopId },
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
budgetRecovery = {
|
|
556
|
+
target: safeLevel,
|
|
557
|
+
promptPath: promptRow?.path ?? null,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Lifecycle signals are always available — never 409'd.
|
|
563
|
+
const summaryEntry = lifecycle.find((e) => e.scheme === "summarize");
|
|
564
|
+
const updateEntry = lifecycle.find((e) => e.scheme === "update");
|
|
565
|
+
let summaryText = summaryEntry?.body || null;
|
|
566
|
+
let updateText = updateEntry?.body || null;
|
|
567
|
+
|
|
568
|
+
// If model sent both, update wins — if it can't decide, it's not done
|
|
569
|
+
if (summaryText && updateText) summaryText = null;
|
|
570
|
+
|
|
571
|
+
// If model says "done" but actions failed, override — the model's
|
|
572
|
+
// assertion that it's done is false if it failed to do what it tried.
|
|
573
|
+
if (summaryText && hasErrors) {
|
|
574
|
+
console.warn(
|
|
575
|
+
"[RUMMY] Overriding <summarize> — actions in this turn failed. Continuing.",
|
|
576
|
+
);
|
|
577
|
+
// Mark the recorded summarize entry as 409 so the model sees it was rejected
|
|
578
|
+
if (summaryEntry?.path) {
|
|
579
|
+
await this.#knownStore.resolve(
|
|
580
|
+
currentRunId,
|
|
581
|
+
summaryEntry.path,
|
|
582
|
+
409,
|
|
583
|
+
"Overridden — actions in this turn failed. Use <update/> until resolved.",
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
updateText = summaryText;
|
|
390
587
|
summaryText = null;
|
|
391
|
-
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// If model sent neither, heal from content
|
|
591
|
+
let statusHealed = false;
|
|
592
|
+
if (!summaryText && !updateText) {
|
|
593
|
+
const healed = ResponseHealer.healStatus(content, commands);
|
|
594
|
+
summaryText = healed.summaryText;
|
|
595
|
+
updateText = healed.updateText;
|
|
596
|
+
statusHealed = true;
|
|
392
597
|
}
|
|
393
598
|
|
|
394
599
|
// --- Classify for return value ---
|
|
395
600
|
|
|
396
601
|
const actionCalls = recorded.filter((e) =>
|
|
397
|
-
["get", "
|
|
602
|
+
["get", "set", "rm", "mv", "cp", "sh", "env", "search"].includes(
|
|
398
603
|
e.scheme,
|
|
399
604
|
),
|
|
400
605
|
);
|
|
@@ -416,7 +621,7 @@ export default class TurnExecutor {
|
|
|
416
621
|
|
|
417
622
|
const askUserEntry = recorded.find((e) => e.scheme === "ask_user");
|
|
418
623
|
|
|
419
|
-
|
|
624
|
+
const turnResult = {
|
|
420
625
|
turn,
|
|
421
626
|
turnId: turnRow.id,
|
|
422
627
|
actionCalls,
|
|
@@ -429,12 +634,16 @@ export default class TurnExecutor {
|
|
|
429
634
|
flags,
|
|
430
635
|
model: result.model || requestedModel,
|
|
431
636
|
modelAlias: requestedModel,
|
|
432
|
-
temperature:
|
|
433
|
-
options?.temperature ??
|
|
434
|
-
Number.parseFloat(process.env.RUMMY_TEMPERATURE || "0.7"),
|
|
637
|
+
temperature: options?.temperature,
|
|
435
638
|
contextSize,
|
|
639
|
+
assembledTokens,
|
|
436
640
|
usage: result.usage,
|
|
641
|
+
budgetRecovery,
|
|
437
642
|
};
|
|
643
|
+
|
|
644
|
+
await this.#hooks.turn.completed.emit(turnResult);
|
|
645
|
+
|
|
646
|
+
return turnResult;
|
|
438
647
|
}
|
|
439
648
|
|
|
440
649
|
/**
|
|
@@ -442,16 +651,17 @@ export default class TurnExecutor {
|
|
|
442
651
|
* Returns the recorded entry descriptor, or null if rejected/skipped.
|
|
443
652
|
*/
|
|
444
653
|
async #record(runId, loopId, turn, mode, cmd) {
|
|
445
|
-
// Mode enforcement — reject prohibited commands in ask mode
|
|
446
654
|
if (mode === "ask") {
|
|
447
655
|
if (cmd.name === "sh") {
|
|
448
656
|
console.warn("[RUMMY] Rejected <sh> in ask mode");
|
|
449
657
|
return null;
|
|
450
658
|
}
|
|
451
|
-
if (cmd.name === "set" && cmd.path) {
|
|
659
|
+
if (cmd.name === "set" && cmd.path && cmd.body) {
|
|
452
660
|
const scheme = KnownStore.scheme(cmd.path);
|
|
453
661
|
if (scheme === null) {
|
|
454
|
-
console.warn(
|
|
662
|
+
console.warn(
|
|
663
|
+
`[RUMMY] Rejected file edit to ${cmd.path} in ${mode} mode`,
|
|
664
|
+
);
|
|
455
665
|
return null;
|
|
456
666
|
}
|
|
457
667
|
}
|
|
@@ -475,15 +685,32 @@ export default class TurnExecutor {
|
|
|
475
685
|
|
|
476
686
|
const scheme = cmd.name;
|
|
477
687
|
|
|
478
|
-
// Structural tags —
|
|
688
|
+
// Structural tags — recorded like any other entry
|
|
479
689
|
if (scheme === "summarize" || scheme === "update") {
|
|
480
|
-
|
|
690
|
+
const statusPath = await this.#knownStore.slugPath(
|
|
691
|
+
runId,
|
|
692
|
+
scheme,
|
|
693
|
+
cmd.body,
|
|
694
|
+
);
|
|
695
|
+
await this.#knownStore.upsert(runId, turn, statusPath, cmd.body, 200, {
|
|
696
|
+
loopId,
|
|
697
|
+
});
|
|
698
|
+
return {
|
|
699
|
+
scheme,
|
|
700
|
+
body: cmd.body,
|
|
701
|
+
path: statusPath,
|
|
702
|
+
resultPath: statusPath,
|
|
703
|
+
attributes: null,
|
|
704
|
+
};
|
|
481
705
|
}
|
|
482
706
|
|
|
483
707
|
// Unknown — deduplicated, sticky
|
|
484
708
|
if (scheme === "unknown") {
|
|
485
709
|
const existingValues = await this.#knownStore.getUnknownValues(runId);
|
|
486
|
-
if (existingValues.has(cmd.body))
|
|
710
|
+
if (existingValues.has(cmd.body)) {
|
|
711
|
+
console.warn(`[RUMMY] Unknown deduped: "${cmd.body.slice(0, 60)}"`);
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
487
714
|
const unknownPath = await this.#knownStore.slugPath(
|
|
488
715
|
runId,
|
|
489
716
|
"unknown",
|
|
@@ -502,8 +729,38 @@ export default class TurnExecutor {
|
|
|
502
729
|
}
|
|
503
730
|
|
|
504
731
|
const rawTarget = cmd.path || cmd.command || cmd.question || "";
|
|
732
|
+
// Reject paths that are likely reasoning bleed — too long or contain non-printing chars
|
|
733
|
+
if (rawTarget.length > 512 || /\p{Cc}/u.test(rawTarget)) {
|
|
734
|
+
const rejectPath = await this.#knownStore.dedup(
|
|
735
|
+
runId,
|
|
736
|
+
scheme,
|
|
737
|
+
`${scheme}://invalid`,
|
|
738
|
+
turn,
|
|
739
|
+
);
|
|
740
|
+
await this.#knownStore.upsert(
|
|
741
|
+
runId,
|
|
742
|
+
turn,
|
|
743
|
+
rejectPath,
|
|
744
|
+
`Invalid path: too long or contains non-printing characters`,
|
|
745
|
+
400,
|
|
746
|
+
{ loopId },
|
|
747
|
+
);
|
|
748
|
+
return {
|
|
749
|
+
scheme,
|
|
750
|
+
path: rejectPath,
|
|
751
|
+
body: "",
|
|
752
|
+
attributes: {},
|
|
753
|
+
status: 400,
|
|
754
|
+
resultPath: rejectPath,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
505
757
|
const target = rawTarget;
|
|
506
|
-
const resultPath = await this.#knownStore.dedup(
|
|
758
|
+
const resultPath = await this.#knownStore.dedup(
|
|
759
|
+
runId,
|
|
760
|
+
scheme,
|
|
761
|
+
target,
|
|
762
|
+
turn,
|
|
763
|
+
);
|
|
507
764
|
|
|
508
765
|
// Pass parsed command fields through as attributes
|
|
509
766
|
const { name: _, ...attributes } = cmd;
|
|
@@ -512,9 +769,72 @@ export default class TurnExecutor {
|
|
|
512
769
|
// known tool or naked write → known:// slug from body
|
|
513
770
|
if (scheme === "known" || (scheme === "set" && !cmd.path)) {
|
|
514
771
|
if (!cmd.body) return null;
|
|
515
|
-
|
|
516
|
-
|
|
772
|
+
|
|
773
|
+
// Size gate: reject entries > 512 tokens — force atomic entries
|
|
774
|
+
const entryTokens = countTokens(cmd.body);
|
|
775
|
+
const MAX_ENTRY_TOKENS = 512;
|
|
776
|
+
if (scheme === "known" && entryTokens > MAX_ENTRY_TOKENS) {
|
|
777
|
+
const rejectPath = await this.#knownStore.slugPath(
|
|
778
|
+
runId,
|
|
779
|
+
scheme,
|
|
780
|
+
cmd.body,
|
|
781
|
+
);
|
|
782
|
+
await this.#knownStore.upsert(
|
|
783
|
+
runId,
|
|
784
|
+
turn,
|
|
785
|
+
rejectPath,
|
|
786
|
+
`Entry too large (${entryTokens} tokens, max ${MAX_ENTRY_TOKENS}). Sort the information, ideas, or plans carefully into multiple entries.`,
|
|
787
|
+
413,
|
|
788
|
+
{ loopId },
|
|
789
|
+
);
|
|
790
|
+
return {
|
|
791
|
+
scheme,
|
|
792
|
+
path: rejectPath,
|
|
793
|
+
body: "",
|
|
794
|
+
resultPath: rejectPath,
|
|
795
|
+
attributes,
|
|
796
|
+
status: 413,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let knownPath = cmd.path;
|
|
801
|
+
if (!knownPath) {
|
|
802
|
+
knownPath = await this.#knownStore.slugPath(
|
|
803
|
+
runId,
|
|
804
|
+
"known",
|
|
805
|
+
cmd.body,
|
|
806
|
+
cmd.summary,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
// Dedup: if this exact path already exists, update rather than duplicate
|
|
810
|
+
const existing = await this.#knownStore.getEntriesByPattern(
|
|
811
|
+
runId,
|
|
812
|
+
knownPath,
|
|
813
|
+
null,
|
|
814
|
+
);
|
|
815
|
+
if (existing.length > 0) {
|
|
816
|
+
// Path exists — update body and turn, skip creating a new entry
|
|
817
|
+
await this.#knownStore.upsert(
|
|
818
|
+
runId,
|
|
819
|
+
turn,
|
|
820
|
+
existing[0].path,
|
|
821
|
+
cmd.body || existing[0].body,
|
|
822
|
+
200,
|
|
823
|
+
{
|
|
824
|
+
attributes,
|
|
825
|
+
loopId,
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
return {
|
|
829
|
+
scheme: "known",
|
|
830
|
+
path: existing[0].path,
|
|
831
|
+
body: cmd.body || existing[0].body,
|
|
832
|
+
resultPath: existing[0].path,
|
|
833
|
+
attributes,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
517
836
|
await this.#knownStore.upsert(runId, turn, knownPath, cmd.body, 200, {
|
|
837
|
+
attributes,
|
|
518
838
|
loopId,
|
|
519
839
|
});
|
|
520
840
|
return {
|
|
@@ -526,20 +846,35 @@ export default class TurnExecutor {
|
|
|
526
846
|
};
|
|
527
847
|
}
|
|
528
848
|
|
|
529
|
-
// Record the entry — 200 OK, handlers change status during dispatch
|
|
530
849
|
const body = cmd.body || cmd.command || cmd.question || "";
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
850
|
+
|
|
851
|
+
// Filter: plugins can validate/transform before recording
|
|
852
|
+
const filtered = await this.#hooks.entry.recording.filter(
|
|
853
|
+
{ scheme, path: resultPath, body, attributes, status: 200 },
|
|
854
|
+
{ runId, turn, loopId },
|
|
855
|
+
);
|
|
856
|
+
if (filtered.status >= 400) return filtered;
|
|
857
|
+
|
|
858
|
+
// Record the entry — 200 OK, handlers change status during dispatch
|
|
859
|
+
await this.#knownStore.upsert(
|
|
860
|
+
runId,
|
|
861
|
+
turn,
|
|
862
|
+
filtered.path,
|
|
863
|
+
filtered.body,
|
|
864
|
+
200,
|
|
865
|
+
{
|
|
866
|
+
attributes: filtered.attributes,
|
|
867
|
+
loopId,
|
|
868
|
+
},
|
|
869
|
+
);
|
|
535
870
|
|
|
536
871
|
return {
|
|
537
|
-
scheme,
|
|
538
|
-
path:
|
|
539
|
-
body,
|
|
540
|
-
attributes,
|
|
872
|
+
scheme: filtered.scheme,
|
|
873
|
+
path: filtered.path,
|
|
874
|
+
body: filtered.body,
|
|
875
|
+
attributes: filtered.attributes,
|
|
541
876
|
status: 200,
|
|
542
|
-
resultPath,
|
|
877
|
+
resultPath: filtered.path,
|
|
543
878
|
};
|
|
544
879
|
}
|
|
545
880
|
}
|