@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
|
@@ -85,6 +85,8 @@ CREATE TABLE IF NOT EXISTS turns (
|
|
|
85
85
|
, run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
|
|
86
86
|
, loop_id INTEGER NOT NULL REFERENCES loops (id) ON DELETE CASCADE
|
|
87
87
|
, sequence INTEGER NOT NULL CHECK (sequence >= 1)
|
|
88
|
+
, context_tokens INTEGER NOT NULL DEFAULT 0 CHECK (context_tokens >= 0)
|
|
89
|
+
, reasoning_content TEXT
|
|
88
90
|
, prompt_tokens INTEGER NOT NULL DEFAULT 0 CHECK (prompt_tokens >= 0)
|
|
89
91
|
, cached_tokens INTEGER NOT NULL DEFAULT 0 CHECK (cached_tokens >= 0)
|
|
90
92
|
, completion_tokens INTEGER NOT NULL DEFAULT 0 CHECK (completion_tokens >= 0)
|
|
@@ -118,12 +120,12 @@ CREATE TABLE IF NOT EXISTS known_entries (
|
|
|
118
120
|
, run_id INTEGER NOT NULL REFERENCES runs (id) ON DELETE CASCADE
|
|
119
121
|
, loop_id INTEGER REFERENCES loops (id) ON DELETE CASCADE
|
|
120
122
|
, turn INTEGER NOT NULL DEFAULT 0 CHECK (turn >= 0)
|
|
121
|
-
, path TEXT NOT NULL
|
|
123
|
+
, path TEXT NOT NULL CHECK (length(path) <= 2048)
|
|
122
124
|
, body TEXT NOT NULL DEFAULT ''
|
|
123
125
|
, scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
|
|
124
126
|
, status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
|
|
125
127
|
, fidelity TEXT NOT NULL DEFAULT 'full' CHECK (
|
|
126
|
-
fidelity IN ('full', 'summary', 'index', '
|
|
128
|
+
fidelity IN ('full', 'summary', 'index', 'archive')
|
|
127
129
|
)
|
|
128
130
|
, hash TEXT
|
|
129
131
|
, attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
|
|
@@ -169,7 +171,7 @@ CREATE TABLE IF NOT EXISTS turn_context (
|
|
|
169
171
|
, body TEXT NOT NULL DEFAULT ''
|
|
170
172
|
, tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
|
|
171
173
|
, attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
|
|
172
|
-
, category TEXT NOT NULL DEFAULT '
|
|
174
|
+
, category TEXT NOT NULL DEFAULT 'logging'
|
|
173
175
|
, source_turn INTEGER DEFAULT 0
|
|
174
176
|
);
|
|
175
177
|
CREATE INDEX IF NOT EXISTS idx_turn_context_run_turn
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@possumtech/rummy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Relational Unknowns Memory Management Yoke",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm"
|
|
@@ -37,10 +37,21 @@
|
|
|
37
37
|
"fix:sql": ". .venv/bin/activate && sqlfluff fix . --force",
|
|
38
38
|
"test": "npm run lint && npm run test:unit && npm run test:intg",
|
|
39
39
|
"test:all": "npm run lint && npm run test:unit && npm run test:intg && npm run test:e2e",
|
|
40
|
-
"test:unit": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --experimental-test-coverage --test-coverage-lines=
|
|
40
|
+
"test:unit": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --experimental-test-coverage --test-coverage-lines=50 --test-coverage-branches=50 --test-coverage-functions=50 --test-concurrency=1 --test-force-exit --test $(find src -name '*.test.js')",
|
|
41
41
|
"test:intg": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test $(find test/integration -name '*.test.js')",
|
|
42
|
-
"test:e2e": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/e2e -name '*.test.js')",
|
|
43
|
-
"test:live": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/live -name '*.test.js')"
|
|
42
|
+
"test:e2e": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/e2e -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/e2e_$(date +%Y%m%dT%H%M%S).log",
|
|
43
|
+
"test:live": "mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --test-concurrency=1 --test-force-exit --test-reporter=spec --test $(find test/live -name '*.test.js') 2>&1 | tee /tmp/rummy_test_diag/live_$(date +%Y%m%dT%H%M%S).log",
|
|
44
|
+
"test:clean": "rm -rf test/lme/results test/mab/results test/tmp /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal && echo 'Test artifacts cleaned.'",
|
|
45
|
+
"test:mab:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/download.js",
|
|
46
|
+
"test:mab": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/mab_$(date +%Y%m%dT%H%M%S).log' --",
|
|
47
|
+
"test:grok": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --env-file-if-exists=.env.grok test/mab/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/mab_grok_$(date +%Y%m%dT%H%M%S).log' --",
|
|
48
|
+
"test:mab:taxonomy": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/mab/runner.js --split Conflict_Resolution --row 0 --no-questions 2>&1 | tee /tmp/rummy_test_diag/taxonomy_$(date +%Y%m%dT%H%M%S).log' --",
|
|
49
|
+
"test:grok:taxonomy": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test --env-file-if-exists=.env.grok test/mab/runner.js --split Conflict_Resolution --row 0 --no-questions 2>&1 | tee /tmp/rummy_test_diag/taxonomy_grok_$(date +%Y%m%dT%H%M%S).log' --",
|
|
50
|
+
"test:lme:get": "node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/download.js",
|
|
51
|
+
"test:lme": "bash -c 'mkdir -p /tmp/rummy_test_diag && node --env-file-if-exists=.env.example --env-file-if-exists=.env --env-file-if-exists=.env.test test/lme/runner.js \"$@\" 2>&1 | tee /tmp/rummy_test_diag/lme_$(date +%Y%m%dT%H%M%S).log' --",
|
|
52
|
+
"test:mab:clean": "rm -rf test/mab/results/*/",
|
|
53
|
+
"test:lme:clean": "rm -rf test/lme/results/*/",
|
|
54
|
+
"test:clear": "rm -rf /tmp/rummy_test_diag /tmp/rummy_test_*.db /tmp/rummy_test_*.db-shm /tmp/rummy_test_*.db-wal /tmp/rummy-stories-*"
|
|
44
55
|
},
|
|
45
56
|
"devDependencies": {
|
|
46
57
|
"@biomejs/biome": "^2.4.6"
|
|
@@ -48,8 +59,9 @@
|
|
|
48
59
|
"dependencies": {
|
|
49
60
|
"@possumtech/sqlrite": "^3.1.0",
|
|
50
61
|
"@xmldom/xmldom": "^0.9.9",
|
|
62
|
+
"diff": "^8.0.4",
|
|
51
63
|
"htmlparser2": "^12.0.0",
|
|
52
|
-
"
|
|
64
|
+
"picomatch": "^4.0.4",
|
|
53
65
|
"ws": "^8.19.0",
|
|
54
66
|
"xpath": "^0.0.34"
|
|
55
67
|
}
|
package/service.js
CHANGED
|
@@ -18,13 +18,13 @@ if (gitCheck.error || gitCheck.status !== 0) {
|
|
|
18
18
|
console.warn("[RUMMY] WARNING: 'git' not found. File tracking will use manual activation only.");
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
let SqlRite, SocketServer, registerPlugins, createHooks, RpcRegistry;
|
|
21
|
+
let SqlRite, SocketServer, registerPlugins, initPlugins, createHooks, RpcRegistry;
|
|
22
22
|
try {
|
|
23
23
|
SqlRite = (await import("@possumtech/sqlrite")).default;
|
|
24
24
|
SocketServer = (await import("./src/server/SocketServer.js")).default;
|
|
25
25
|
const pluginIndex = await import("./src/plugins/index.js");
|
|
26
26
|
registerPlugins = pluginIndex.registerPlugins;
|
|
27
|
-
|
|
27
|
+
initPlugins = pluginIndex.initPlugins;
|
|
28
28
|
createHooks = (await import("./src/hooks/Hooks.js")).default;
|
|
29
29
|
RpcRegistry = (await import("./src/server/RpcRegistry.js")).default;
|
|
30
30
|
} catch (err) {
|
|
@@ -81,10 +81,12 @@ async function main() {
|
|
|
81
81
|
if (!key.startsWith("RUMMY_MODEL_")) continue;
|
|
82
82
|
const alias = key.replace("RUMMY_MODEL_", "");
|
|
83
83
|
const actual = process.env[key];
|
|
84
|
+
const contextEnv = process.env[`RUMMY_CONTEXT_${alias}`];
|
|
85
|
+
const context_length = contextEnv ? Number.parseInt(contextEnv, 10) : null;
|
|
84
86
|
await db.upsert_model.get({
|
|
85
87
|
alias,
|
|
86
88
|
actual,
|
|
87
|
-
context_length
|
|
89
|
+
context_length,
|
|
88
90
|
});
|
|
89
91
|
modelAliases.push(alias);
|
|
90
92
|
}
|
package/src/agent/AgentLoop.js
CHANGED
|
@@ -32,7 +32,7 @@ export default class AgentLoop {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
async #ensureRun(projectId, model, run, options) {
|
|
35
|
-
const
|
|
35
|
+
const _noRepo = options?.noRepo === true;
|
|
36
36
|
const isFork = options?.fork === true;
|
|
37
37
|
const requestedModel = model;
|
|
38
38
|
|
|
@@ -54,6 +54,11 @@ export default class AgentLoop {
|
|
|
54
54
|
new_run_id: runRow.id,
|
|
55
55
|
parent_run_id: existingRun.id,
|
|
56
56
|
});
|
|
57
|
+
await this.#hooks.run.created.emit({
|
|
58
|
+
runId: runRow.id,
|
|
59
|
+
alias,
|
|
60
|
+
forkedFrom: existingRun.id,
|
|
61
|
+
});
|
|
57
62
|
return { runId: runRow.id, alias };
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -87,6 +92,7 @@ export default class AgentLoop {
|
|
|
87
92
|
persona: options?.persona ?? null,
|
|
88
93
|
context_limit: options?.contextLimit ?? null,
|
|
89
94
|
});
|
|
95
|
+
await this.#hooks.run.created.emit({ runId: runRow.id, alias });
|
|
90
96
|
return { runId: runRow.id, alias };
|
|
91
97
|
}
|
|
92
98
|
|
|
@@ -112,7 +118,10 @@ export default class AgentLoop {
|
|
|
112
118
|
if (!project)
|
|
113
119
|
throw new Error(msg("error.project_not_found", { projectId }));
|
|
114
120
|
|
|
115
|
-
const
|
|
121
|
+
const noRepo = options?.noRepo === true;
|
|
122
|
+
const noInteraction = options?.noInteraction === true;
|
|
123
|
+
const noWeb = options?.noWeb === true;
|
|
124
|
+
const noProposals = options?.noProposals === true;
|
|
116
125
|
const requestedModel = model;
|
|
117
126
|
|
|
118
127
|
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
@@ -134,7 +143,13 @@ export default class AgentLoop {
|
|
|
134
143
|
mode,
|
|
135
144
|
model: requestedModel,
|
|
136
145
|
prompt: prompt || "",
|
|
137
|
-
config: JSON.stringify({
|
|
146
|
+
config: JSON.stringify({
|
|
147
|
+
noRepo,
|
|
148
|
+
noInteraction,
|
|
149
|
+
noWeb,
|
|
150
|
+
noProposals,
|
|
151
|
+
temperature: options?.temperature,
|
|
152
|
+
}),
|
|
138
153
|
});
|
|
139
154
|
|
|
140
155
|
if (this.#activeRuns.has(currentRunId)) {
|
|
@@ -151,38 +166,76 @@ export default class AgentLoop {
|
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
async #drainQueue(currentRunId, currentAlias, projectId, project, options) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
run_id: currentRunId,
|
|
157
|
-
});
|
|
158
|
-
if (!loop) break;
|
|
159
|
-
|
|
160
|
-
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
161
|
-
const result = await this.#executeLoop({
|
|
162
|
-
mode: loop.mode,
|
|
163
|
-
project,
|
|
164
|
-
projectId,
|
|
165
|
-
currentRunId,
|
|
166
|
-
currentAlias,
|
|
167
|
-
currentLoopId: loop.id,
|
|
168
|
-
requestedModel: loop.model,
|
|
169
|
-
prompt: loop.prompt,
|
|
170
|
-
noContext: loopConfig.noContext || false,
|
|
171
|
-
options: { ...options, temperature: loopConfig.temperature },
|
|
172
|
-
hook: loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act,
|
|
173
|
-
});
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
this.#activeRuns.set(currentRunId, controller);
|
|
174
171
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
const loop = await this.#db.claim_next_loop.get({
|
|
175
|
+
run_id: currentRunId,
|
|
176
|
+
});
|
|
177
|
+
if (!loop) break;
|
|
178
|
+
|
|
179
|
+
const loopConfig = loop.config ? JSON.parse(loop.config) : {};
|
|
180
|
+
const hook = loop.mode === "ask" ? this.#hooks.ask : this.#hooks.act;
|
|
181
|
+
|
|
182
|
+
let result;
|
|
183
|
+
try {
|
|
184
|
+
result = await this.#executeLoop({
|
|
185
|
+
mode: loop.mode,
|
|
186
|
+
project,
|
|
187
|
+
projectId,
|
|
188
|
+
currentRunId,
|
|
189
|
+
currentAlias,
|
|
190
|
+
currentLoopId: loop.id,
|
|
191
|
+
requestedModel: loop.model,
|
|
192
|
+
prompt: loop.prompt,
|
|
193
|
+
noRepo: loopConfig.noRepo || false,
|
|
194
|
+
noInteraction: loopConfig.noInteraction || false,
|
|
195
|
+
noWeb: loopConfig.noWeb || false,
|
|
196
|
+
noProposals: loopConfig.noProposals || false,
|
|
197
|
+
options: { ...options, temperature: loopConfig.temperature },
|
|
198
|
+
hook,
|
|
199
|
+
signal: controller.signal,
|
|
200
|
+
});
|
|
201
|
+
} catch (err) {
|
|
202
|
+
await this.#db.complete_loop.run({
|
|
203
|
+
id: loop.id,
|
|
204
|
+
status: 500,
|
|
205
|
+
result: JSON.stringify({ error: err.message }),
|
|
206
|
+
});
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
180
209
|
|
|
181
|
-
|
|
182
|
-
|
|
210
|
+
if (result.status === 413) {
|
|
211
|
+
await this.#db.complete_loop.run({
|
|
212
|
+
id: loop.id,
|
|
213
|
+
status: 413,
|
|
214
|
+
result: JSON.stringify(result),
|
|
215
|
+
});
|
|
216
|
+
return {
|
|
217
|
+
run: currentAlias,
|
|
218
|
+
status: 413,
|
|
219
|
+
error: `Context full (${result.overflow} tokens over).`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await this.#db.complete_loop.run({
|
|
224
|
+
id: loop.id,
|
|
225
|
+
status: result.status === 202 ? 202 : result.status,
|
|
226
|
+
result: JSON.stringify(result),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (result.status === 202) return result;
|
|
230
|
+
}
|
|
183
231
|
|
|
184
|
-
|
|
185
|
-
|
|
232
|
+
const runRow = await this.#db.get_run_by_alias.get({
|
|
233
|
+
alias: currentAlias,
|
|
234
|
+
});
|
|
235
|
+
return { run: currentAlias, status: runRow?.status ?? 200 };
|
|
236
|
+
} finally {
|
|
237
|
+
this.#activeRuns.delete(currentRunId);
|
|
238
|
+
}
|
|
186
239
|
}
|
|
187
240
|
|
|
188
241
|
async #executeLoop({
|
|
@@ -194,9 +247,13 @@ export default class AgentLoop {
|
|
|
194
247
|
currentLoopId,
|
|
195
248
|
requestedModel,
|
|
196
249
|
prompt,
|
|
197
|
-
|
|
250
|
+
noRepo,
|
|
251
|
+
noInteraction,
|
|
252
|
+
noWeb,
|
|
253
|
+
noProposals,
|
|
198
254
|
options,
|
|
199
255
|
hook,
|
|
256
|
+
signal,
|
|
200
257
|
}) {
|
|
201
258
|
const runRow = await this.#db.get_run_by_id.get({ id: currentRunId });
|
|
202
259
|
if (runRow.status !== 102) {
|
|
@@ -212,16 +269,41 @@ export default class AgentLoop {
|
|
|
212
269
|
? Math.min(runRow.context_limit, modelContextSize)
|
|
213
270
|
: modelContextSize;
|
|
214
271
|
|
|
272
|
+
const toolSet = this.#hooks.tools.resolveForLoop(mode, {
|
|
273
|
+
noInteraction,
|
|
274
|
+
noWeb,
|
|
275
|
+
noProposals,
|
|
276
|
+
});
|
|
277
|
+
|
|
215
278
|
let loopIteration = 0;
|
|
216
279
|
const MAX_LOOP_ITERATIONS = Number(process.env.RUMMY_MAX_TURNS) || 15;
|
|
217
280
|
const healer = new ResponseHealer();
|
|
218
281
|
|
|
219
|
-
|
|
220
|
-
|
|
282
|
+
let _lastAssembledTokens = 0;
|
|
283
|
+
let recovery = null; // { target, promptPath, strikes, lastTokens }
|
|
284
|
+
|
|
285
|
+
// Demote full logging entries from previous loops to summary before
|
|
286
|
+
// they appear in <previous>. General policy: keep <previous> compact.
|
|
287
|
+
await this.#knownStore.demotePreviousLoopLogging(
|
|
288
|
+
currentRunId,
|
|
289
|
+
currentLoopId,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// Restore any prompt entries left at summary fidelity by a recovery
|
|
293
|
+
// phase that was interrupted (server crash, restart). If the full
|
|
294
|
+
// prompt would overflow, Prompt Demotion on turn 1 handles it.
|
|
295
|
+
await this.#knownStore.restoreSummarizedPrompts(currentRunId);
|
|
296
|
+
|
|
297
|
+
await this.#hooks.loop.started.emit({
|
|
298
|
+
runId: currentRunId,
|
|
299
|
+
loopId: currentLoopId,
|
|
300
|
+
mode,
|
|
301
|
+
prompt,
|
|
302
|
+
});
|
|
221
303
|
|
|
222
304
|
try {
|
|
223
305
|
while (loopIteration < MAX_LOOP_ITERATIONS) {
|
|
224
|
-
if (
|
|
306
|
+
if (signal.aborted) {
|
|
225
307
|
await this.#db.update_run_status.run({
|
|
226
308
|
id: currentRunId,
|
|
227
309
|
status: 499,
|
|
@@ -255,12 +337,52 @@ export default class AgentLoop {
|
|
|
255
337
|
currentLoopId,
|
|
256
338
|
requestedModel,
|
|
257
339
|
loopPrompt: turnPrompt,
|
|
258
|
-
|
|
340
|
+
loopIteration,
|
|
341
|
+
noRepo,
|
|
342
|
+
toolSet,
|
|
343
|
+
inRecovery: recovery !== null,
|
|
259
344
|
contextSize,
|
|
260
345
|
options: { ...options, isContinuation: loopIteration > 1 },
|
|
261
|
-
signal
|
|
346
|
+
signal,
|
|
262
347
|
});
|
|
263
348
|
|
|
349
|
+
if (result.status === 413) {
|
|
350
|
+
return {
|
|
351
|
+
run: currentAlias,
|
|
352
|
+
status: 413,
|
|
353
|
+
overflow: result.overflow,
|
|
354
|
+
assembledTokens: result.assembledTokens,
|
|
355
|
+
contextSize: result.contextSize,
|
|
356
|
+
turn: result.turn,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
_lastAssembledTokens = result.assembledTokens;
|
|
361
|
+
|
|
362
|
+
// Budget recovery: enforce progress toward context target.
|
|
363
|
+
const ra = advanceRecovery(recovery, result);
|
|
364
|
+
recovery = ra.next;
|
|
365
|
+
if (ra.action === "restore" && ra.promptPath) {
|
|
366
|
+
await this.#knownStore.setFidelity(
|
|
367
|
+
currentRunId,
|
|
368
|
+
ra.promptPath,
|
|
369
|
+
"full",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
if (ra.action === "hard413") {
|
|
373
|
+
await this.#db.update_run_status.run({
|
|
374
|
+
id: currentRunId,
|
|
375
|
+
status: 413,
|
|
376
|
+
});
|
|
377
|
+
const out = {
|
|
378
|
+
run: currentAlias,
|
|
379
|
+
status: 413,
|
|
380
|
+
turn: result.turn,
|
|
381
|
+
};
|
|
382
|
+
await hook.completed.emit({ projectId, ...out });
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
264
386
|
const runUsage = await this.#db.get_run_usage.get({
|
|
265
387
|
run_id: currentRunId,
|
|
266
388
|
});
|
|
@@ -292,12 +414,13 @@ export default class AgentLoop {
|
|
|
292
414
|
model: result.model,
|
|
293
415
|
temperature: result.temperature,
|
|
294
416
|
context_size: result.contextSize,
|
|
295
|
-
context_tokens:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
417
|
+
context_tokens:
|
|
418
|
+
(
|
|
419
|
+
await this.#db.get_turn_context_tokens.get({
|
|
420
|
+
run_id: currentRunId,
|
|
421
|
+
sequence: result.turn,
|
|
422
|
+
})
|
|
423
|
+
)?.context_tokens ?? 0,
|
|
301
424
|
prompt_tokens: runUsage.prompt_tokens,
|
|
302
425
|
cached_tokens: runUsage.cached_tokens,
|
|
303
426
|
completion_tokens: runUsage.completion_tokens,
|
|
@@ -332,6 +455,9 @@ export default class AgentLoop {
|
|
|
332
455
|
flags: result.flags,
|
|
333
456
|
});
|
|
334
457
|
|
|
458
|
+
// Don't exit while budget recovery is still active.
|
|
459
|
+
if (recovery !== null) continue;
|
|
460
|
+
|
|
335
461
|
const repetition = healer.assessRepetition(result);
|
|
336
462
|
if (!repetition.continue) {
|
|
337
463
|
await this.#db.update_run_status.run({
|
|
@@ -376,7 +502,7 @@ export default class AgentLoop {
|
|
|
376
502
|
await hook.completed.emit({ projectId, ...out });
|
|
377
503
|
return out;
|
|
378
504
|
} catch (err) {
|
|
379
|
-
if (
|
|
505
|
+
if (signal.aborted) {
|
|
380
506
|
await this.#db.update_run_status.run({
|
|
381
507
|
id: currentRunId,
|
|
382
508
|
status: 499,
|
|
@@ -408,7 +534,14 @@ export default class AgentLoop {
|
|
|
408
534
|
await hook.completed.emit({ projectId, ...out });
|
|
409
535
|
return out;
|
|
410
536
|
} finally {
|
|
411
|
-
this.#
|
|
537
|
+
await this.#hooks.loop.completed
|
|
538
|
+
.emit({
|
|
539
|
+
runId: currentRunId,
|
|
540
|
+
loopId: currentLoopId,
|
|
541
|
+
mode,
|
|
542
|
+
turns: loopIteration,
|
|
543
|
+
})
|
|
544
|
+
.catch(() => {});
|
|
412
545
|
}
|
|
413
546
|
}
|
|
414
547
|
|
|
@@ -441,6 +574,29 @@ export default class AgentLoop {
|
|
|
441
574
|
}
|
|
442
575
|
|
|
443
576
|
if (action === "accept") {
|
|
577
|
+
if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
|
|
578
|
+
const fileBody = await this.#knownStore.getBody(runId, attrs.file);
|
|
579
|
+
if (fileBody != null) {
|
|
580
|
+
const blocks = attrs.merge.split(/(?=<<<<<<< SEARCH)/);
|
|
581
|
+
let patched = fileBody;
|
|
582
|
+
for (const block of blocks) {
|
|
583
|
+
const m = block.match(
|
|
584
|
+
/<<<<<<< SEARCH\n?([\s\S]*?)\n?=======\n?([\s\S]*?)\n?>>>>>>> REPLACE/,
|
|
585
|
+
);
|
|
586
|
+
if (m) patched = patched.replace(m[1], m[2]);
|
|
587
|
+
}
|
|
588
|
+
const turn = (await this.#db.get_run_by_id.get({ id: runId }))
|
|
589
|
+
.next_turn;
|
|
590
|
+
await this.#knownStore.upsert(
|
|
591
|
+
runId,
|
|
592
|
+
turn,
|
|
593
|
+
attrs.file,
|
|
594
|
+
patched,
|
|
595
|
+
200,
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
444
600
|
if (path.startsWith("rm://")) {
|
|
445
601
|
if (attrs?.path) {
|
|
446
602
|
await this.#knownStore.remove(runId, attrs.path);
|
|
@@ -518,7 +674,7 @@ export default class AgentLoop {
|
|
|
518
674
|
mode: resumeMode,
|
|
519
675
|
model: runRow.model,
|
|
520
676
|
prompt: "",
|
|
521
|
-
config: "{}",
|
|
677
|
+
config: currentLoop?.config || "{}",
|
|
522
678
|
});
|
|
523
679
|
return this.#drainQueue(runId, runAlias, projectId, project, {});
|
|
524
680
|
}
|
|
@@ -546,16 +702,9 @@ export default class AgentLoop {
|
|
|
546
702
|
runRow.id,
|
|
547
703
|
nextTurn,
|
|
548
704
|
`prompt://${nextTurn}`,
|
|
549
|
-
"",
|
|
550
|
-
200,
|
|
551
|
-
{ attributes: { mode: "ask" } },
|
|
552
|
-
);
|
|
553
|
-
await this.#knownStore.upsert(
|
|
554
|
-
runRow.id,
|
|
555
|
-
nextTurn,
|
|
556
|
-
`ask://${nextTurn}`,
|
|
557
705
|
message,
|
|
558
706
|
200,
|
|
707
|
+
{ attributes: { mode: "ask" } },
|
|
559
708
|
);
|
|
560
709
|
|
|
561
710
|
if (this.#activeRuns.has(runRow.id)) {
|
|
@@ -584,3 +733,51 @@ export default class AgentLoop {
|
|
|
584
733
|
return this.#knownStore.getLog(runRow.id);
|
|
585
734
|
}
|
|
586
735
|
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Pure recovery state transition — exported for testing.
|
|
739
|
+
*
|
|
740
|
+
* @param {object|null} recovery Current recovery state (mutated copy returned).
|
|
741
|
+
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
742
|
+
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
743
|
+
*/
|
|
744
|
+
export function advanceRecovery(recovery, result) {
|
|
745
|
+
// Initialise or update recovery state from a new Turn Demotion event.
|
|
746
|
+
if (result.budgetRecovery) {
|
|
747
|
+
if (!recovery) {
|
|
748
|
+
recovery = {
|
|
749
|
+
target: result.budgetRecovery.target,
|
|
750
|
+
promptPath: result.budgetRecovery.promptPath,
|
|
751
|
+
strikes: 0,
|
|
752
|
+
lastTokens: result.assembledTokens,
|
|
753
|
+
};
|
|
754
|
+
} else {
|
|
755
|
+
// Re-overflow during recovery: tighten target, don't count as strike.
|
|
756
|
+
recovery = {
|
|
757
|
+
...recovery,
|
|
758
|
+
target: Math.min(recovery.target, result.budgetRecovery.target),
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (recovery === null) return { next: null, action: null, promptPath: null };
|
|
764
|
+
|
|
765
|
+
const current = result.assembledTokens;
|
|
766
|
+
|
|
767
|
+
if (current <= recovery.target) {
|
|
768
|
+
return { next: null, action: "restore", promptPath: recovery.promptPath };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const noProgress = current >= recovery.lastTokens && !result.budgetRecovery;
|
|
772
|
+
const strikes = noProgress ? recovery.strikes + 1 : 0;
|
|
773
|
+
|
|
774
|
+
if (strikes >= 3) {
|
|
775
|
+
return { next: null, action: "hard413", promptPath: null };
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
next: { ...recovery, strikes, lastTokens: current },
|
|
780
|
+
action: null,
|
|
781
|
+
promptPath: null,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
@@ -6,17 +6,33 @@
|
|
|
6
6
|
export default class ContextAssembler {
|
|
7
7
|
static async assembleFromTurnContext(
|
|
8
8
|
rows,
|
|
9
|
-
{
|
|
9
|
+
{
|
|
10
|
+
type = "ask",
|
|
11
|
+
systemPrompt = "",
|
|
12
|
+
contextSize = 0,
|
|
13
|
+
demoted = [],
|
|
14
|
+
toolSet = null,
|
|
15
|
+
lastContextTokens = 0,
|
|
16
|
+
turn = 1,
|
|
17
|
+
} = {},
|
|
10
18
|
hooks,
|
|
11
19
|
) {
|
|
12
20
|
// Find loop boundary from active prompt
|
|
13
21
|
const promptEntry = rows.findLast(
|
|
14
|
-
(r) =>
|
|
15
|
-
r.category === "prompt" && (r.scheme === "ask" || r.scheme === "act"),
|
|
22
|
+
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
16
23
|
);
|
|
17
24
|
const loopStartTurn = promptEntry?.source_turn ?? 0;
|
|
18
25
|
|
|
19
|
-
const ctx = {
|
|
26
|
+
const ctx = {
|
|
27
|
+
rows,
|
|
28
|
+
loopStartTurn,
|
|
29
|
+
type,
|
|
30
|
+
contextSize,
|
|
31
|
+
lastContextTokens,
|
|
32
|
+
demoted,
|
|
33
|
+
toolSet,
|
|
34
|
+
turn,
|
|
35
|
+
};
|
|
20
36
|
|
|
21
37
|
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|
|
22
38
|
const user = await hooks.assembly.user.filter("", ctx);
|