@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
package/EXCEPTIONS.md
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
# EXCEPTIONS.md — Documented Backbone Responsibilities
|
|
2
|
-
|
|
3
|
-
Operations that bypass the plugin protocol. Each must justify WHY it
|
|
4
|
-
can't go through the standard tool handler path. If the justification
|
|
5
|
-
is weak, the exception should be eliminated.
|
|
6
|
-
|
|
7
|
-
## Resolved
|
|
8
|
-
|
|
9
|
-
### File.setConstraint / File.dropConstraint
|
|
10
|
-
|
|
11
|
-
**What:** Direct DB writes to `file_constraints` table.
|
|
12
|
-
**Justification:** File constraints are project-level config — they
|
|
13
|
-
define which files a project cares about. This is backbone, not tool
|
|
14
|
-
dispatch. Entry promotion/demotion that follows constraints now goes
|
|
15
|
-
through the standard tool handler chain via `dispatchTool`.
|
|
16
|
-
**Boundary documented:** SPEC.md §2.3.
|
|
17
|
-
|
|
18
|
-
## Currently Identified
|
|
19
|
-
|
|
20
|
-
### 1. TurnExecutor#record — tool-specific handling
|
|
21
|
-
|
|
22
|
-
**What:** `known`, `unknown`, `summarize`, `update` have special-case
|
|
23
|
-
code in `#record` (dedup, slug paths, lifecycle classification).
|
|
24
|
-
**Bypasses:** These tools don't go through the same dispatch path as
|
|
25
|
-
`get`, `set`, `rm` etc.
|
|
26
|
-
**Justification:** Lifecycle signals (`summarize`, `update`) are state
|
|
27
|
-
declarations, not tool operations — they always dispatch and cannot be
|
|
28
|
-
409'd. `known` and `unknown` generate their own paths from body content
|
|
29
|
-
(slug paths). The classification is a fundamental architectural split
|
|
30
|
-
(lifecycle vs action), not a protocol violation.
|
|
31
|
-
|
|
32
|
-
### 2. Token math — multiple measurement points
|
|
33
|
-
|
|
34
|
-
**What:** `known_entries.tokens`, `turn_context.tokens`,
|
|
35
|
-
`turns.context_tokens`, `countTokens()` estimates.
|
|
36
|
-
**Bypasses:** No single function call, but a strict rule.
|
|
37
|
-
**Justification:** Each serves a different purpose. `known_entries.tokens`
|
|
38
|
-
is display-only (model sees entry sizes in `<knowns>`). `turn_context.tokens`
|
|
39
|
-
is per-turn snapshot. `turns.context_tokens` is assembled ground truth for
|
|
40
|
-
budget. The rule: budget decisions use ONLY assembled message tokens.
|
|
41
|
-
DB tokens are NEVER used for budget. Documented in PLUGINS.md §7.5.
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
*This file should shrink over time. Every entry is a debt to be paid
|
|
46
|
-
or a boundary to be justified.*
|
package/src/agent/KnownStore.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import slugify from "../sql/functions/slugify.js";
|
|
2
|
-
|
|
3
|
-
export default class KnownStore {
|
|
4
|
-
#db;
|
|
5
|
-
#onChanged;
|
|
6
|
-
#schemes = new Map();
|
|
7
|
-
#seq = 0;
|
|
8
|
-
#pendingResolutions = new Map();
|
|
9
|
-
|
|
10
|
-
constructor(db, { onChanged } = {}) {
|
|
11
|
-
this.#db = db;
|
|
12
|
-
this.#onChanged = onChanged || null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async loadSchemes(db) {
|
|
16
|
-
const rows = await (db || this.#db).get_all_schemes.all();
|
|
17
|
-
this.#schemes.clear();
|
|
18
|
-
for (const row of rows) {
|
|
19
|
-
this.#schemes.set(row.name, row);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
#emitChanged(runId, path, changeType) {
|
|
24
|
-
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
static scheme(path) {
|
|
28
|
-
const idx = path.indexOf("://");
|
|
29
|
-
return idx > 0 ? path.slice(0, idx) : null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
static normalizePath(path) {
|
|
33
|
-
if (!path?.includes("://")) return path;
|
|
34
|
-
const sep = path.indexOf("://");
|
|
35
|
-
const scheme = path.slice(0, sep).toLowerCase();
|
|
36
|
-
const rest = path.slice(sep + 3);
|
|
37
|
-
try {
|
|
38
|
-
// Decode first (idempotent), then encode — but preserve slashes
|
|
39
|
-
const decoded = decodeURIComponent(rest);
|
|
40
|
-
return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
|
|
41
|
-
} catch {
|
|
42
|
-
return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async nextTurn(runId) {
|
|
47
|
-
const row = await this.#db.next_turn.get({ run_id: runId });
|
|
48
|
-
return row.turn;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async dedup(runId, scheme, target, turn) {
|
|
52
|
-
const encodedTarget = encodeURIComponent(target);
|
|
53
|
-
const turnPrefix = turn ? `turn_${turn}/` : "";
|
|
54
|
-
const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
|
|
55
|
-
const existing = await this.#db.get_entry_body.get({
|
|
56
|
-
run_id: runId,
|
|
57
|
-
path: candidate,
|
|
58
|
-
});
|
|
59
|
-
if (!existing) return candidate;
|
|
60
|
-
return `${candidate}_${++this.#seq}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async slugPath(runId, scheme, content, summary) {
|
|
64
|
-
const source = summary || content || "";
|
|
65
|
-
const base = slugify(source);
|
|
66
|
-
const prefix = `${scheme}://`;
|
|
67
|
-
|
|
68
|
-
if (!base) return `${prefix}${++this.#seq}`;
|
|
69
|
-
|
|
70
|
-
const candidate = `${prefix}${base}`;
|
|
71
|
-
const existing = await this.#db.get_entry_body.get({
|
|
72
|
-
run_id: runId,
|
|
73
|
-
path: candidate,
|
|
74
|
-
});
|
|
75
|
-
if (!existing) return candidate;
|
|
76
|
-
|
|
77
|
-
return `${prefix}${base}_${++this.#seq}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
async upsert(
|
|
81
|
-
runId,
|
|
82
|
-
turn,
|
|
83
|
-
path,
|
|
84
|
-
body,
|
|
85
|
-
status,
|
|
86
|
-
{
|
|
87
|
-
fidelity = "full",
|
|
88
|
-
attributes = null,
|
|
89
|
-
hash = null,
|
|
90
|
-
updatedAt = null,
|
|
91
|
-
loopId = null,
|
|
92
|
-
} = {},
|
|
93
|
-
) {
|
|
94
|
-
const normalized = KnownStore.normalizePath(path);
|
|
95
|
-
await this.#db.upsert_known_entry.run({
|
|
96
|
-
run_id: runId,
|
|
97
|
-
loop_id: loopId,
|
|
98
|
-
turn,
|
|
99
|
-
path: normalized,
|
|
100
|
-
body,
|
|
101
|
-
status,
|
|
102
|
-
fidelity,
|
|
103
|
-
hash,
|
|
104
|
-
attributes: attributes ? JSON.stringify(attributes) : null,
|
|
105
|
-
updated_at: updatedAt,
|
|
106
|
-
});
|
|
107
|
-
this.#emitChanged(runId, normalized, "upsert");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async promote(runId, path, turn) {
|
|
111
|
-
const normalized = KnownStore.normalizePath(path);
|
|
112
|
-
await this.#db.promote_path.run({
|
|
113
|
-
run_id: runId,
|
|
114
|
-
path: normalized,
|
|
115
|
-
turn,
|
|
116
|
-
});
|
|
117
|
-
this.#emitChanged(runId, normalized, "promote");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async setFileFidelity(runId, pattern, fidelity) {
|
|
121
|
-
const result = await this.#db.set_file_fidelity.run({
|
|
122
|
-
run_id: runId,
|
|
123
|
-
pattern,
|
|
124
|
-
fidelity,
|
|
125
|
-
});
|
|
126
|
-
if (result.changes === 0) {
|
|
127
|
-
await this.upsert(runId, 0, pattern, "", 200, { fidelity });
|
|
128
|
-
}
|
|
129
|
-
this.#emitChanged(runId, pattern, "fidelity");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async setFidelity(runId, path, fidelity) {
|
|
133
|
-
const normalized = KnownStore.normalizePath(path);
|
|
134
|
-
await this.#db.set_fidelity.run({
|
|
135
|
-
run_id: runId,
|
|
136
|
-
path: normalized,
|
|
137
|
-
fidelity,
|
|
138
|
-
});
|
|
139
|
-
this.#emitChanged(runId, normalized, "fidelity");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async demote(runId, path) {
|
|
143
|
-
const normalized = KnownStore.normalizePath(path);
|
|
144
|
-
await this.#db.demote_path.run({
|
|
145
|
-
run_id: runId,
|
|
146
|
-
path: normalized,
|
|
147
|
-
});
|
|
148
|
-
this.#emitChanged(runId, normalized, "demote");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async remove(runId, path) {
|
|
152
|
-
const normalized = KnownStore.normalizePath(path);
|
|
153
|
-
await this.#db.delete_known_entry.run({
|
|
154
|
-
run_id: runId,
|
|
155
|
-
path: normalized,
|
|
156
|
-
});
|
|
157
|
-
this.#emitChanged(runId, normalized, "remove");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async removeFilesByPattern(runId, pattern) {
|
|
161
|
-
await this.#db.delete_file_entries_by_pattern.run({
|
|
162
|
-
run_id: runId,
|
|
163
|
-
pattern,
|
|
164
|
-
});
|
|
165
|
-
this.#emitChanged(runId, pattern, "remove");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
static #bodyPattern(body) {
|
|
169
|
-
return body || null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async promoteByPattern(runId, path, body, turn) {
|
|
173
|
-
await this.#db.promote_by_pattern.run({
|
|
174
|
-
run_id: runId,
|
|
175
|
-
path,
|
|
176
|
-
body: KnownStore.#bodyPattern(body),
|
|
177
|
-
turn,
|
|
178
|
-
});
|
|
179
|
-
this.#emitChanged(runId, path, "promote");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async demoteByPattern(runId, path, body) {
|
|
183
|
-
await this.#db.demote_by_pattern.run({
|
|
184
|
-
run_id: runId,
|
|
185
|
-
path,
|
|
186
|
-
body: KnownStore.#bodyPattern(body),
|
|
187
|
-
});
|
|
188
|
-
this.#emitChanged(runId, path, "demote");
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async getEntriesByPattern(runId, path, body, { limit, offset } = {}) {
|
|
192
|
-
return this.#db.get_entries_by_pattern.all({
|
|
193
|
-
run_id: runId,
|
|
194
|
-
path,
|
|
195
|
-
body: KnownStore.#bodyPattern(body),
|
|
196
|
-
limit: limit ?? null,
|
|
197
|
-
offset: offset ?? null,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
async deleteByPattern(runId, path, body) {
|
|
202
|
-
await this.#db.delete_entries_by_pattern.run({
|
|
203
|
-
run_id: runId,
|
|
204
|
-
path,
|
|
205
|
-
body: KnownStore.#bodyPattern(body),
|
|
206
|
-
});
|
|
207
|
-
this.#emitChanged(runId, path, "remove");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async updateBodyByPattern(runId, path, body, newBody) {
|
|
211
|
-
await this.#db.update_body_by_pattern.run({
|
|
212
|
-
run_id: runId,
|
|
213
|
-
path,
|
|
214
|
-
body: KnownStore.#bodyPattern(body),
|
|
215
|
-
new_body: newBody,
|
|
216
|
-
});
|
|
217
|
-
this.#emitChanged(runId, path, "body");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async resolve(runId, path, status, body) {
|
|
221
|
-
const normalized = KnownStore.normalizePath(path);
|
|
222
|
-
await this.#db.resolve_known_entry.run({
|
|
223
|
-
run_id: runId,
|
|
224
|
-
path: normalized,
|
|
225
|
-
status,
|
|
226
|
-
body,
|
|
227
|
-
});
|
|
228
|
-
this.#emitChanged(runId, normalized, "resolve");
|
|
229
|
-
const key = `${runId}:${normalized}`;
|
|
230
|
-
const resolver = this.#pendingResolutions.get(key);
|
|
231
|
-
if (resolver) {
|
|
232
|
-
this.#pendingResolutions.delete(key);
|
|
233
|
-
resolver();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
waitForResolution(runId, path) {
|
|
238
|
-
const normalized = KnownStore.normalizePath(path);
|
|
239
|
-
const key = `${runId}:${normalized}`;
|
|
240
|
-
return new Promise((resolve) => {
|
|
241
|
-
this.#pendingResolutions.set(key, resolve);
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async restoreSummarizedPrompts(runId) {
|
|
246
|
-
await this.#db.restore_summarized_prompts.run({ run_id: runId });
|
|
247
|
-
this.#emitChanged(runId, "prompt://batch", "fidelity");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
async getLog(runId) {
|
|
252
|
-
return this.#db.get_results.all({ run_id: runId });
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
async getEntries(runId) {
|
|
256
|
-
return this.#db.get_known_entries.all({ run_id: runId });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async getFileEntries(runId) {
|
|
260
|
-
return this.#db.get_file_entries.all({ run_id: runId });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async getFileStatesByPattern(runId, pattern) {
|
|
264
|
-
return this.#db.get_file_states_by_pattern.all({ run_id: runId, pattern });
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async hasRejections(runId, loopId) {
|
|
268
|
-
const row = await this.#db.has_rejections.get({
|
|
269
|
-
run_id: runId,
|
|
270
|
-
loop_id: loopId,
|
|
271
|
-
});
|
|
272
|
-
return row.count > 0;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
async hasAcceptedActions(runId) {
|
|
276
|
-
const row = await this.#db.has_accepted_actions.get({ run_id: runId });
|
|
277
|
-
return row.count > 0;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async getUnresolved(runId) {
|
|
281
|
-
return this.#db.get_unresolved.all({ run_id: runId });
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async countUnknowns(runId) {
|
|
285
|
-
const row = await this.#db.count_unknowns.get({ run_id: runId });
|
|
286
|
-
return row.count;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
async getUnknownValues(runId) {
|
|
290
|
-
const rows = await this.#db.get_unknown_values.all({ run_id: runId });
|
|
291
|
-
return new Set(rows.map((r) => r.body));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async getBody(runId, path) {
|
|
295
|
-
const row = await this.#db.get_entry_body.get({
|
|
296
|
-
run_id: runId,
|
|
297
|
-
path: KnownStore.normalizePath(path),
|
|
298
|
-
});
|
|
299
|
-
return row?.body ?? null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async setAttributes(runId, path, attrs) {
|
|
303
|
-
const normalized = KnownStore.normalizePath(path);
|
|
304
|
-
await this.#db.update_entry_attributes.run({
|
|
305
|
-
run_id: runId,
|
|
306
|
-
path: normalized,
|
|
307
|
-
attributes: JSON.stringify(attrs),
|
|
308
|
-
});
|
|
309
|
-
this.#emitChanged(runId, normalized, "attributes");
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
async getState(runId, path) {
|
|
313
|
-
return this.#db.get_entry_state.get({
|
|
314
|
-
run_id: runId,
|
|
315
|
-
path: KnownStore.normalizePath(path),
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async getAttributes(runId, path) {
|
|
320
|
-
const row = await this.#db.get_entry_attributes.get({
|
|
321
|
-
run_id: runId,
|
|
322
|
-
path: KnownStore.normalizePath(path),
|
|
323
|
-
});
|
|
324
|
-
return row?.attributes ? JSON.parse(row.attributes) : null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async getTurnAudit(runId, turn) {
|
|
328
|
-
return this.#db.get_turn_audit.all({ run_id: runId, turn });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
static toolFromPath(path) {
|
|
332
|
-
return KnownStore.scheme(path);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
static isSystemPath(path) {
|
|
336
|
-
return path.includes("://");
|
|
337
|
-
}
|
|
338
|
-
}
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
const MAX_STALLS = Number(process.env.RUMMY_MAX_STALLS) || 3;
|
|
2
|
-
const MIN_CYCLES = Number(process.env.RUMMY_MIN_CYCLES) || 3;
|
|
3
|
-
const MAX_CYCLE_PERIOD = Number(process.env.RUMMY_MAX_CYCLE_PERIOD) || 4;
|
|
4
|
-
const MAX_UPDATE_REPEATS = Number(process.env.RUMMY_MAX_UPDATE_REPEATS) || 3;
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Build a stable fingerprint for a single recorded entry.
|
|
8
|
-
* Uses scheme + original command target + all op-defining attributes.
|
|
9
|
-
* Excludes body (content too granular; same operation ≠ same content).
|
|
10
|
-
*/
|
|
11
|
-
function cmdFingerprint(entry) {
|
|
12
|
-
const attrs = { ...(entry.attributes ?? {}) };
|
|
13
|
-
delete attrs.body;
|
|
14
|
-
const target =
|
|
15
|
-
attrs.path ?? attrs.command ?? attrs.query ?? attrs.question ?? "";
|
|
16
|
-
delete attrs.path;
|
|
17
|
-
const extra = Object.keys(attrs)
|
|
18
|
-
.toSorted()
|
|
19
|
-
.filter((k) => attrs[k] != null)
|
|
20
|
-
.map((k) => `${k}=${attrs[k]}`)
|
|
21
|
-
.join(",");
|
|
22
|
-
return `${entry.scheme}:${target}${extra ? `[${extra}]` : ""}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Detect a repeating cycle in the fingerprint history.
|
|
27
|
-
* Checks periods 1..MAX_CYCLE_PERIOD for MIN_CYCLES consecutive repetitions.
|
|
28
|
-
* Catches AAAA (period 1), ABABAB (period 2), ABCABCABC (period 3), etc.
|
|
29
|
-
*/
|
|
30
|
-
function detectCycle(history) {
|
|
31
|
-
for (let k = 1; k <= MAX_CYCLE_PERIOD; k++) {
|
|
32
|
-
const needed = k * MIN_CYCLES;
|
|
33
|
-
if (history.length < needed) continue;
|
|
34
|
-
const tail = history.slice(-needed);
|
|
35
|
-
const cycle = tail.slice(0, k);
|
|
36
|
-
let match = true;
|
|
37
|
-
outer: for (let rep = 0; rep < MIN_CYCLES; rep++) {
|
|
38
|
-
for (let j = 0; j < k; j++) {
|
|
39
|
-
if (tail[rep * k + j] !== cycle[j]) {
|
|
40
|
-
match = false;
|
|
41
|
-
break outer;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
if (match) return { detected: true, period: k, cycles: MIN_CYCLES };
|
|
46
|
-
}
|
|
47
|
-
return { detected: false };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export default class ResponseHealer {
|
|
51
|
-
#stallCount = 0;
|
|
52
|
-
#turnHistory = [];
|
|
53
|
-
#lastUpdateText = null;
|
|
54
|
-
#updateRepeatCount = 0;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Heal a missing status tag. Called when the model emits
|
|
58
|
-
* neither <summarize/> nor <update/>.
|
|
59
|
-
*/
|
|
60
|
-
/**
|
|
61
|
-
* Heal a missing status tag. Called when the model emits
|
|
62
|
-
* neither <summarize/> nor <update/>.
|
|
63
|
-
*
|
|
64
|
-
* Plain text with no commands = the model answered. Treat as summary.
|
|
65
|
-
* Commands with no status tag = the model is working. Treat as update.
|
|
66
|
-
*/
|
|
67
|
-
static healStatus(content, commands) {
|
|
68
|
-
const trimmed = content.trim();
|
|
69
|
-
|
|
70
|
-
// No commands + plain text = answered. Treat as summary.
|
|
71
|
-
if (commands.length === 0 && trimmed) {
|
|
72
|
-
console.warn("[RUMMY] Healed: plain text response treated as summary");
|
|
73
|
-
return { summaryText: trimmed.slice(0, 500), updateText: null };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Only write/unknown commands + no investigation tools = completed action.
|
|
77
|
-
// The model did the thing without saying <summarize>. Treat as summary.
|
|
78
|
-
const hasInvestigation = commands.some((c) =>
|
|
79
|
-
["get", "env", "search", "ask_user"].includes(c.name),
|
|
80
|
-
);
|
|
81
|
-
if (!hasInvestigation && commands.length > 0) {
|
|
82
|
-
const names = commands.map((c) => c.name).join(", ");
|
|
83
|
-
console.warn(
|
|
84
|
-
`[RUMMY] Healed: action-only response (${names}) treated as summary`,
|
|
85
|
-
);
|
|
86
|
-
return {
|
|
87
|
-
summaryText: trimmed.slice(0, 500) || "Done.",
|
|
88
|
-
updateText: null,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
console.warn(
|
|
93
|
-
`[RUMMY] Healed: missing <update>/<summarize>. Tools: ${commands.map((c) => c.name).join(", ") || "none"}`,
|
|
94
|
-
);
|
|
95
|
-
return { summaryText: null, updateText: "..." };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Detect cyclic tool patterns across turns.
|
|
100
|
-
* Returns { continue: boolean, reason?: string }
|
|
101
|
-
*
|
|
102
|
-
* Appends this turn's fingerprint to history, then checks whether the
|
|
103
|
-
* history ends in a repeating cycle of period 1..MAX_CYCLE_PERIOD with
|
|
104
|
-
* at least MIN_CYCLES consecutive repetitions.
|
|
105
|
-
*
|
|
106
|
-
* Catches AAAA (period 1), ABABAB (period 2), ABCABC (period 3), etc.
|
|
107
|
-
* Turns with no tool calls are skipped — they don't contribute to a cycle.
|
|
108
|
-
*/
|
|
109
|
-
assessRepetition({ actionCalls, writeCalls }) {
|
|
110
|
-
const commands = [...(actionCalls || []), ...(writeCalls || [])];
|
|
111
|
-
if (commands.length === 0) return { continue: true };
|
|
112
|
-
|
|
113
|
-
const fp = commands.map(cmdFingerprint).toSorted().join("|");
|
|
114
|
-
this.#turnHistory.push(fp);
|
|
115
|
-
|
|
116
|
-
const cycle = detectCycle(this.#turnHistory);
|
|
117
|
-
if (cycle.detected) {
|
|
118
|
-
const reason = `Cyclic tool pattern (period ${cycle.period}, ${cycle.cycles} repetitions)`;
|
|
119
|
-
console.warn(`[RUMMY] Loop detected: ${reason}. Force-completing.`);
|
|
120
|
-
return { continue: false, reason };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return { continue: true };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Assess whether the run should continue.
|
|
128
|
-
*
|
|
129
|
-
* Returns { continue: boolean, reason?: string }
|
|
130
|
-
*
|
|
131
|
-
* Rules:
|
|
132
|
-
* <summarize/> present → done (terminate)
|
|
133
|
-
* <summarize/> + failed actions → overridden to <update> (continue)
|
|
134
|
-
* <update/> present → continue (model says it's working)
|
|
135
|
-
* neither present → warn, increment stall counter, continue
|
|
136
|
-
* stall counter hits MAX_STALLS → force-complete
|
|
137
|
-
*/
|
|
138
|
-
assessProgress({ summaryText, updateText, statusHealed, flags }) {
|
|
139
|
-
if (summaryText) {
|
|
140
|
-
this.#stallCount = 0;
|
|
141
|
-
return { continue: false };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (updateText && !statusHealed) {
|
|
145
|
-
this.#stallCount = 0;
|
|
146
|
-
// Track repeated update text — model stuck declaring readiness
|
|
147
|
-
// But if the model created new entries this turn, it's making
|
|
148
|
-
// progress even if the update text is the same.
|
|
149
|
-
const madeProgress = flags?.hasWrites || flags?.hasReads;
|
|
150
|
-
if (updateText === this.#lastUpdateText && !madeProgress) {
|
|
151
|
-
this.#updateRepeatCount++;
|
|
152
|
-
if (this.#updateRepeatCount >= MAX_UPDATE_REPEATS) {
|
|
153
|
-
const reason = `Same <update/> repeated ${this.#updateRepeatCount} turns: "${updateText.slice(0, 60)}"`;
|
|
154
|
-
console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
|
|
155
|
-
return { continue: false, reason };
|
|
156
|
-
}
|
|
157
|
-
} else {
|
|
158
|
-
this.#lastUpdateText = updateText;
|
|
159
|
-
this.#updateRepeatCount = 1;
|
|
160
|
-
}
|
|
161
|
-
return { continue: true };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Healed or neither — model is glitching
|
|
165
|
-
this.#stallCount++;
|
|
166
|
-
|
|
167
|
-
if (this.#stallCount >= MAX_STALLS) {
|
|
168
|
-
const reason = `${this.#stallCount} turns with no <update/> or <summarize/>`;
|
|
169
|
-
console.warn(`[RUMMY] Stalled: ${reason}. Force-completing.`);
|
|
170
|
-
return { continue: false, reason };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
console.warn(
|
|
174
|
-
`[RUMMY] No <update/> or <summarize/> (stall ${this.#stallCount}/${MAX_STALLS})`,
|
|
175
|
-
);
|
|
176
|
-
return { continue: true };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Reset state for a new run or after resolution resume.
|
|
181
|
-
*/
|
|
182
|
-
reset() {
|
|
183
|
-
this.#stallCount = 0;
|
|
184
|
-
this.#turnHistory = [];
|
|
185
|
-
this.#lastUpdateText = null;
|
|
186
|
-
this.#updateRepeatCount = 0;
|
|
187
|
-
}
|
|
188
|
-
}
|
package/src/llm/OpenAiClient.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import msg from "../agent/messages.js";
|
|
2
|
-
|
|
3
|
-
export default class OpenAiClient {
|
|
4
|
-
#baseUrl;
|
|
5
|
-
#apiKey;
|
|
6
|
-
|
|
7
|
-
constructor(baseUrl, apiKey) {
|
|
8
|
-
this.#baseUrl = String(baseUrl || "").replace(/\/v1\/?$/, "");
|
|
9
|
-
this.#apiKey = apiKey || "";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async completion(messages, model, options = {}) {
|
|
13
|
-
const body = { model, messages, think: true };
|
|
14
|
-
if (options.temperature !== undefined)
|
|
15
|
-
body.temperature = options.temperature;
|
|
16
|
-
|
|
17
|
-
const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
|
|
18
|
-
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
19
|
-
const signal = options.signal
|
|
20
|
-
? AbortSignal.any([options.signal, timeoutSignal])
|
|
21
|
-
: timeoutSignal;
|
|
22
|
-
|
|
23
|
-
const headers = { "Content-Type": "application/json" };
|
|
24
|
-
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
25
|
-
|
|
26
|
-
const response = await fetch(`${this.#baseUrl}/v1/chat/completions`, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers,
|
|
29
|
-
body: JSON.stringify(body),
|
|
30
|
-
signal,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
if (!response.ok) {
|
|
34
|
-
const error = await response.text();
|
|
35
|
-
throw new Error(
|
|
36
|
-
msg("error.openai_api", { status: `${response.status} - ${error}` }),
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const data = await response.json();
|
|
41
|
-
|
|
42
|
-
for (const choice of data.choices || []) {
|
|
43
|
-
const msg = choice.message;
|
|
44
|
-
if (!msg) continue;
|
|
45
|
-
|
|
46
|
-
// Normalize reasoning
|
|
47
|
-
const parts = [msg.reasoning_content, msg.reasoning, msg.thinking].filter(
|
|
48
|
-
Boolean,
|
|
49
|
-
);
|
|
50
|
-
msg.reasoning_content =
|
|
51
|
-
parts.length > 0 ? [...new Set(parts)].join("\n") : null;
|
|
52
|
-
|
|
53
|
-
if (process.env.RUMMY_DEBUG === "true" && msg.reasoning_content) {
|
|
54
|
-
console.warn(
|
|
55
|
-
`[RUMMY] Reasoning (${msg.reasoning_content.length} chars): ${msg.reasoning_content.slice(0, 120)}`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return data;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async getContextSize(_model) {
|
|
64
|
-
const timeout = Number(process.env.RUMMY_FETCH_TIMEOUT) || 30_000;
|
|
65
|
-
const headers = { "Content-Type": "application/json" };
|
|
66
|
-
if (this.#apiKey) headers.Authorization = `Bearer ${this.#apiKey}`;
|
|
67
|
-
|
|
68
|
-
// Try /props first — llama.cpp exposes runtime n_ctx here
|
|
69
|
-
try {
|
|
70
|
-
const propsResponse = await fetch(`${this.#baseUrl}/props`, {
|
|
71
|
-
headers,
|
|
72
|
-
signal: AbortSignal.timeout(timeout),
|
|
73
|
-
});
|
|
74
|
-
if (propsResponse.ok) {
|
|
75
|
-
const props = await propsResponse.json();
|
|
76
|
-
const runtimeCtx = props?.default_generation_settings?.n_ctx;
|
|
77
|
-
if (runtimeCtx) return runtimeCtx;
|
|
78
|
-
}
|
|
79
|
-
} catch {}
|
|
80
|
-
|
|
81
|
-
// Fall back to /v1/models for training context
|
|
82
|
-
const response = await fetch(`${this.#baseUrl}/v1/models`, {
|
|
83
|
-
headers,
|
|
84
|
-
signal: AbortSignal.timeout(timeout),
|
|
85
|
-
});
|
|
86
|
-
if (!response.ok) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
msg("error.openai_models_failed", {
|
|
89
|
-
status: response.status,
|
|
90
|
-
baseUrl: this.#baseUrl,
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const data = await response.json();
|
|
95
|
-
const model = data.data?.[0];
|
|
96
|
-
const ctx = model?.meta?.n_ctx_train || model?.context_length;
|
|
97
|
-
if (!ctx) throw new Error(msg("error.openai_no_context_length"));
|
|
98
|
-
return ctx;
|
|
99
|
-
}
|
|
100
|
-
}
|