@possumtech/rummy 0.5.0 → 2.0.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 +42 -5
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +934 -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 +13 -11
- package/scriptify/ask_run.js +77 -0
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +476 -335
- package/src/agent/ContextAssembler.js +4 -4
- package/src/agent/Entries.js +676 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -421
- package/src/agent/XmlParser.js +99 -33
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +280 -125
- package/src/agent/materializeContext.js +104 -0
- package/src/agent/runs.sql +29 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/tokens.js +6 -0
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +29 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +139 -35
- package/src/hooks/ToolRegistry.js +21 -16
- package/src/llm/LlmProvider.js +66 -89
- package/src/llm/errors.js +21 -0
- package/src/llm/retry.js +63 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -25
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -25
- package/src/plugins/budget/budget.js +306 -88
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +29 -11
- package/src/plugins/cp/cpDoc.js +2 -15
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +45 -6
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -2
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +103 -48
- package/src/plugins/get/getDoc.js +2 -32
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +42 -2
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +244 -9
- package/src/plugins/instructions/instructions.md +33 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +38 -0
- package/src/plugins/instructions/instructions_106.md +21 -0
- package/src/plugins/instructions/instructions_107.md +10 -0
- package/src/plugins/instructions/instructions_108.md +0 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +68 -36
- package/src/plugins/known/knownDoc.js +2 -17
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +129 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +55 -22
- package/src/plugins/mv/mvDoc.js +2 -18
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +64 -16
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +56 -12
- package/src/plugins/rm/rmDoc.js +2 -20
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +525 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -75
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +50 -6
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -18
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +129 -80
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +2 -15
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +47 -19
- package/src/plugins/unknown/unknownDoc.js +2 -21
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +83 -5
- package/src/plugins/update/updateDoc.js +2 -30
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/plugins/yolo/yolo.js +192 -0
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/FIDELITY_CONTRACT.md +0 -172
- package/src/agent/KnownStore.js +0 -337
- package/src/agent/ResponseHealer.js +0 -241
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -45
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -56
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -43
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -27
|
@@ -14,15 +14,16 @@ export default class ContextAssembler {
|
|
|
14
14
|
toolSet = null,
|
|
15
15
|
lastContextTokens = 0,
|
|
16
16
|
turn = 1,
|
|
17
|
-
baselineTokens = 0,
|
|
18
17
|
} = {},
|
|
19
18
|
hooks,
|
|
20
19
|
) {
|
|
21
|
-
// Find loop boundary from active prompt
|
|
20
|
+
// Find loop boundary from active prompt. Absent on turn 1 before
|
|
21
|
+
// the prompt plugin's turn.started handler has run.
|
|
22
22
|
const promptEntry = rows.findLast(
|
|
23
23
|
(r) => r.category === "prompt" && r.scheme === "prompt",
|
|
24
24
|
);
|
|
25
|
-
|
|
25
|
+
let loopStartTurn = 0;
|
|
26
|
+
if (promptEntry) loopStartTurn = promptEntry.source_turn;
|
|
26
27
|
|
|
27
28
|
const ctx = {
|
|
28
29
|
rows,
|
|
@@ -33,7 +34,6 @@ export default class ContextAssembler {
|
|
|
33
34
|
demoted,
|
|
34
35
|
toolSet,
|
|
35
36
|
turn,
|
|
36
|
-
baselineTokens,
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
const system = await hooks.assembly.system.filter(systemPrompt, ctx);
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import slugify from "../sql/functions/slugify.js";
|
|
2
|
+
import { PermissionError } from "./errors.js";
|
|
3
|
+
|
|
4
|
+
export default class Entries {
|
|
5
|
+
#db;
|
|
6
|
+
#onChanged;
|
|
7
|
+
#schemes = new Map();
|
|
8
|
+
#schemesLoaded = null;
|
|
9
|
+
#seq = 0;
|
|
10
|
+
#pendingResolutions = new Map();
|
|
11
|
+
|
|
12
|
+
constructor(db, { onChanged = null } = {}) {
|
|
13
|
+
this.#db = db;
|
|
14
|
+
this.#onChanged = onChanged;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Populate the scheme cache. Can be called explicitly (e.g. at boot
|
|
19
|
+
* after initPlugins finishes) or runs lazily on first need. Idempotent.
|
|
20
|
+
*/
|
|
21
|
+
async loadSchemes(db) {
|
|
22
|
+
const rows = await (db || this.#db).get_all_schemes.all();
|
|
23
|
+
this.#schemes.clear();
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
this.#schemes.set(row.name, row);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async #ensureSchemes() {
|
|
30
|
+
if (!this.#schemesLoaded) {
|
|
31
|
+
this.#schemesLoaded = this.loadSchemes();
|
|
32
|
+
}
|
|
33
|
+
return this.#schemesLoaded;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#emitChanged(runId, path, changeType) {
|
|
37
|
+
if (this.#onChanged) this.#onChanged({ runId, path, changeType });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static scheme(path) {
|
|
41
|
+
if (!path) return null;
|
|
42
|
+
const idx = path.indexOf("://");
|
|
43
|
+
return idx > 0 ? path.slice(0, idx) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static normalizePath(path) {
|
|
47
|
+
if (!path?.includes("://")) return path;
|
|
48
|
+
const sep = path.indexOf("://");
|
|
49
|
+
const scheme = path.slice(0, sep).toLowerCase();
|
|
50
|
+
const rest = path.slice(sep + 3);
|
|
51
|
+
try {
|
|
52
|
+
// Decode first (idempotent), then encode — but preserve slashes
|
|
53
|
+
const decoded = decodeURIComponent(rest);
|
|
54
|
+
return `${scheme}://${decoded.split("/").map(encodeURIComponent).join("/")}`;
|
|
55
|
+
} catch {
|
|
56
|
+
return `${scheme}://${rest.split("/").map(encodeURIComponent).join("/")}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async nextTurn(runId) {
|
|
61
|
+
const row = await this.#db.next_turn.get({ run_id: runId });
|
|
62
|
+
return row.turn;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async dedup(runId, scheme, target, turn) {
|
|
66
|
+
const encodedTarget = encodeURIComponent(target);
|
|
67
|
+
const turnPrefix = turn ? `turn_${turn}/` : "";
|
|
68
|
+
const candidate = `${scheme}://${turnPrefix}${encodedTarget}`;
|
|
69
|
+
const existing = await this.#db.get_entry_body.get({
|
|
70
|
+
run_id: runId,
|
|
71
|
+
path: candidate,
|
|
72
|
+
});
|
|
73
|
+
if (!existing) return candidate;
|
|
74
|
+
return `${candidate}_${++this.#seq}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Log entries share a single namespace at log://turn_N/action/slug.
|
|
78
|
+
// The action segment is the tool/plugin name (set, get, search, update,
|
|
79
|
+
// error, etc.). Target is URL-encoded so slashes and scheme separators
|
|
80
|
+
// survive round-trips.
|
|
81
|
+
async logPath(runId, turn, action, target) {
|
|
82
|
+
const encodedTarget = encodeURIComponent(target);
|
|
83
|
+
const candidate = `log://turn_${turn}/${action}/${encodedTarget}`;
|
|
84
|
+
const existing = await this.#db.get_entry_body.get({
|
|
85
|
+
run_id: runId,
|
|
86
|
+
path: candidate,
|
|
87
|
+
});
|
|
88
|
+
if (!existing) return candidate;
|
|
89
|
+
return `${candidate}_${++this.#seq}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async slugPath(runId, scheme, content, summary) {
|
|
93
|
+
// Prefer summary, fall back to body content, then empty — slugify
|
|
94
|
+
// handles empty explicitly by returning "" and the caller generates
|
|
95
|
+
// a sequence-only path.
|
|
96
|
+
let source = "";
|
|
97
|
+
if (summary) source = summary;
|
|
98
|
+
else if (content) source = content;
|
|
99
|
+
const base = slugify(source);
|
|
100
|
+
const prefix = `${scheme}://`;
|
|
101
|
+
|
|
102
|
+
if (!base) return `${prefix}${++this.#seq}`;
|
|
103
|
+
|
|
104
|
+
const candidate = `${prefix}${base}`;
|
|
105
|
+
const existing = await this.#db.get_entry_body.get({
|
|
106
|
+
run_id: runId,
|
|
107
|
+
path: candidate,
|
|
108
|
+
});
|
|
109
|
+
if (!existing) return candidate;
|
|
110
|
+
|
|
111
|
+
return `${prefix}${base}_${++this.#seq}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolve a scheme's declared scope kind + writer list + category.
|
|
116
|
+
* Unregistered or declaration-less schemes default to run-level +
|
|
117
|
+
* model/plugin writers so ad-hoc paths (e.g. bare filenames) still
|
|
118
|
+
* work.
|
|
119
|
+
*/
|
|
120
|
+
async #schemeRules(scheme) {
|
|
121
|
+
await this.#ensureSchemes();
|
|
122
|
+
const row = scheme ? this.#schemes.get(scheme) : null;
|
|
123
|
+
const kind = row?.default_scope ? row.default_scope : "run";
|
|
124
|
+
const category = row?.category ? row.category : "logging";
|
|
125
|
+
let writers = ["model", "plugin"];
|
|
126
|
+
if (row?.writable_by) {
|
|
127
|
+
const parsed =
|
|
128
|
+
typeof row.writable_by === "string"
|
|
129
|
+
? JSON.parse(row.writable_by)
|
|
130
|
+
: row.writable_by;
|
|
131
|
+
if (Array.isArray(parsed)) writers = parsed;
|
|
132
|
+
}
|
|
133
|
+
return { kind, writers, category };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#defaultVisibility(scheme, category) {
|
|
137
|
+
if (scheme === "skill") return "visible";
|
|
138
|
+
if (category === "prompt") return "visible";
|
|
139
|
+
if (category === "unknown") return "visible";
|
|
140
|
+
if (category === "logging") return "visible";
|
|
141
|
+
return "summarized";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
#resolveScope(kind, runId, projectId) {
|
|
145
|
+
if (kind === "global") return "global";
|
|
146
|
+
if (kind === "project") {
|
|
147
|
+
if (!projectId) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
"project-scoped write requires projectId; caller must pass it to set()",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return `project:${projectId}`;
|
|
153
|
+
}
|
|
154
|
+
return `run:${runId}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* set — create or update an entry. The semantically wide primitive.
|
|
159
|
+
*
|
|
160
|
+
* Modes (selected by which options are present):
|
|
161
|
+
* — write content: body given, state ∈ {proposed,streaming,resolved,failed,cancelled}
|
|
162
|
+
* — change visibility only: visibility given, body omitted
|
|
163
|
+
* — change state only: state given, body omitted (resolve a proposal)
|
|
164
|
+
* — merge attributes: attributes given, body omitted
|
|
165
|
+
* — append to body: append:true (streaming)
|
|
166
|
+
* — pattern match: path contains wildcards or bodyFilter set
|
|
167
|
+
*/
|
|
168
|
+
async set({
|
|
169
|
+
runId,
|
|
170
|
+
projectId = null,
|
|
171
|
+
turn = 0,
|
|
172
|
+
path,
|
|
173
|
+
body,
|
|
174
|
+
state,
|
|
175
|
+
visibility,
|
|
176
|
+
outcome = null,
|
|
177
|
+
attributes,
|
|
178
|
+
append,
|
|
179
|
+
bodyFilter = null,
|
|
180
|
+
pattern,
|
|
181
|
+
hash = null,
|
|
182
|
+
loopId = null,
|
|
183
|
+
writer = "plugin",
|
|
184
|
+
}) {
|
|
185
|
+
if (!runId) throw new Error("set: runId is required");
|
|
186
|
+
if (!path) throw new Error("set: path is required");
|
|
187
|
+
|
|
188
|
+
// Pattern mode is explicit (pattern: true) or implicit when a
|
|
189
|
+
// body filter is supplied. The literal `*` character can appear
|
|
190
|
+
// inside legitimate exact paths (e.g. rm://foo%2F* as a result
|
|
191
|
+
// path for an rm against a pattern); we don't infer pattern mode
|
|
192
|
+
// from the path alone.
|
|
193
|
+
const isPattern = pattern === true || bodyFilter !== null;
|
|
194
|
+
|
|
195
|
+
// Pattern mode: update matching entries (visibility / body / both).
|
|
196
|
+
if (isPattern) {
|
|
197
|
+
if (body != null && !append) {
|
|
198
|
+
await this.#db.update_body_by_pattern.run({
|
|
199
|
+
run_id: runId,
|
|
200
|
+
path,
|
|
201
|
+
body: bodyFilter,
|
|
202
|
+
new_body: body,
|
|
203
|
+
});
|
|
204
|
+
await this.#db.bump_write_count_by_pattern.run({
|
|
205
|
+
run_id: runId,
|
|
206
|
+
path,
|
|
207
|
+
body: bodyFilter,
|
|
208
|
+
});
|
|
209
|
+
this.#emitChanged(runId, path, "body");
|
|
210
|
+
}
|
|
211
|
+
if (visibility === "visible") {
|
|
212
|
+
await this.#db.promote_by_pattern.run({
|
|
213
|
+
run_id: runId,
|
|
214
|
+
path,
|
|
215
|
+
body: bodyFilter,
|
|
216
|
+
turn,
|
|
217
|
+
});
|
|
218
|
+
this.#emitChanged(runId, path, "promote");
|
|
219
|
+
} else if (visibility === "summarized" || visibility === "archived") {
|
|
220
|
+
await this.#db.demote_by_pattern.run({
|
|
221
|
+
run_id: runId,
|
|
222
|
+
path,
|
|
223
|
+
body: bodyFilter,
|
|
224
|
+
});
|
|
225
|
+
this.#emitChanged(runId, path, "demote");
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const normalized = Entries.normalizePath(path);
|
|
231
|
+
const scheme = Entries.scheme(normalized);
|
|
232
|
+
|
|
233
|
+
// Append mode: streaming body growth on an existing entry.
|
|
234
|
+
if (append) {
|
|
235
|
+
if (body == null) throw new Error("set: append requires body");
|
|
236
|
+
await this.#db.append_entry_body.run({
|
|
237
|
+
run_id: runId,
|
|
238
|
+
path: normalized,
|
|
239
|
+
chunk: body,
|
|
240
|
+
});
|
|
241
|
+
this.#emitChanged(runId, normalized, "append");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Body-less state or visibility change on an existing entry.
|
|
246
|
+
if (body == null) {
|
|
247
|
+
if (state != null) {
|
|
248
|
+
await this.#db.resolve_known_entry_view.run({
|
|
249
|
+
run_id: runId,
|
|
250
|
+
path: normalized,
|
|
251
|
+
state,
|
|
252
|
+
outcome,
|
|
253
|
+
});
|
|
254
|
+
this.#emitChanged(runId, normalized, "resolve");
|
|
255
|
+
this.#drainPendingResolution(runId, normalized);
|
|
256
|
+
}
|
|
257
|
+
if (visibility != null) {
|
|
258
|
+
await this.#db.set_visibility.run({
|
|
259
|
+
run_id: runId,
|
|
260
|
+
path: normalized,
|
|
261
|
+
visibility,
|
|
262
|
+
});
|
|
263
|
+
this.#emitChanged(runId, normalized, "visibility");
|
|
264
|
+
}
|
|
265
|
+
if (attributes != null) {
|
|
266
|
+
await this.#db.update_entry_attributes.run({
|
|
267
|
+
run_id: runId,
|
|
268
|
+
path: normalized,
|
|
269
|
+
attributes: JSON.stringify(attributes),
|
|
270
|
+
});
|
|
271
|
+
this.#emitChanged(runId, normalized, "attributes");
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Full write/upsert: body + state + visibility + attributes.
|
|
277
|
+
const { kind, writers, category } = await this.#schemeRules(scheme);
|
|
278
|
+
if (!writers.includes(writer)) {
|
|
279
|
+
throw new PermissionError(scheme, writer, writers);
|
|
280
|
+
}
|
|
281
|
+
const scope = this.#resolveScope(kind, runId, projectId);
|
|
282
|
+
// Log entries self-describe via `action` so consumers (renderer,
|
|
283
|
+
// client UIs, tests) can read the action without parsing the
|
|
284
|
+
// path. Only inject `action` when the caller passes attributes
|
|
285
|
+
// — a null `attributes` means "don't touch existing" and the
|
|
286
|
+
// SQL's COALESCE handles preservation on UPDATE. If we generated
|
|
287
|
+
// `{action: m[1]}` for every null-attributes log write, every
|
|
288
|
+
// body-only update to a log entry would clobber existing attrs
|
|
289
|
+
// (command, summary, demotedCount, ...).
|
|
290
|
+
const effectiveAttributes = attributes ? { ...attributes } : null;
|
|
291
|
+
if (scheme === "log" && effectiveAttributes) {
|
|
292
|
+
const m = normalized.match(/^log:\/\/turn_\d+\/([^/]+)\//);
|
|
293
|
+
if (m) effectiveAttributes.action = m[1];
|
|
294
|
+
}
|
|
295
|
+
const entry = await this.#db.upsert_entry.get({
|
|
296
|
+
scope,
|
|
297
|
+
path: normalized,
|
|
298
|
+
body,
|
|
299
|
+
attributes: effectiveAttributes
|
|
300
|
+
? JSON.stringify(effectiveAttributes)
|
|
301
|
+
: null,
|
|
302
|
+
hash,
|
|
303
|
+
});
|
|
304
|
+
const effectiveState = state === undefined ? "resolved" : state;
|
|
305
|
+
const effectiveVisibility =
|
|
306
|
+
visibility === undefined
|
|
307
|
+
? this.#defaultVisibility(scheme, category)
|
|
308
|
+
: visibility;
|
|
309
|
+
await this.#db.upsert_run_view.run({
|
|
310
|
+
run_id: runId,
|
|
311
|
+
entry_id: entry.id,
|
|
312
|
+
loop_id: loopId,
|
|
313
|
+
turn,
|
|
314
|
+
state: effectiveState,
|
|
315
|
+
outcome,
|
|
316
|
+
visibility: effectiveVisibility,
|
|
317
|
+
});
|
|
318
|
+
this.#emitChanged(runId, normalized, "upsert");
|
|
319
|
+
if (effectiveState !== "proposed") {
|
|
320
|
+
this.#drainPendingResolution(runId, normalized);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* get — promote entry(ies) to visible visibility. Default visibility is
|
|
326
|
+
* "visible"; pass visibility explicitly for a read-with-side-effect at
|
|
327
|
+
* a different visibility (rare).
|
|
328
|
+
*/
|
|
329
|
+
async get({
|
|
330
|
+
runId,
|
|
331
|
+
turn = 0,
|
|
332
|
+
path,
|
|
333
|
+
bodyFilter = null,
|
|
334
|
+
visibility = "visible",
|
|
335
|
+
}) {
|
|
336
|
+
if (!runId) throw new Error("get: runId is required");
|
|
337
|
+
if (!path) throw new Error("get: path is required");
|
|
338
|
+
if (visibility === "visible") {
|
|
339
|
+
await this.#db.promote_by_pattern.run({
|
|
340
|
+
run_id: runId,
|
|
341
|
+
path,
|
|
342
|
+
body: bodyFilter,
|
|
343
|
+
turn,
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
await this.#db.demote_by_pattern.run({
|
|
347
|
+
run_id: runId,
|
|
348
|
+
path,
|
|
349
|
+
body: bodyFilter,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
this.#emitChanged(runId, path, "promote");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* rm — remove entry view(s). Matches single path or pattern; optional
|
|
357
|
+
* bodyFilter narrows pattern matches. `filesOnly` restricts to bare
|
|
358
|
+
* file-scheme entries (scheme IS NULL).
|
|
359
|
+
*/
|
|
360
|
+
async rm({ runId, path, bodyFilter = null, filesOnly = false }) {
|
|
361
|
+
if (!runId) throw new Error("rm: runId is required");
|
|
362
|
+
if (!path) throw new Error("rm: path is required");
|
|
363
|
+
if (filesOnly) {
|
|
364
|
+
await this.#db.delete_file_entries_by_pattern.run({
|
|
365
|
+
run_id: runId,
|
|
366
|
+
pattern: path,
|
|
367
|
+
});
|
|
368
|
+
} else if (bodyFilter !== null || /[*?[\]]/.test(path)) {
|
|
369
|
+
await this.#db.delete_entries_by_pattern.run({
|
|
370
|
+
run_id: runId,
|
|
371
|
+
path,
|
|
372
|
+
body: bodyFilter,
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
const normalized = Entries.normalizePath(path);
|
|
376
|
+
await this.#db.delete_known_entry.run({
|
|
377
|
+
run_id: runId,
|
|
378
|
+
path: normalized,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
this.#emitChanged(runId, path, "remove");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* cp — copy an entry to a new path. Source body becomes new body;
|
|
386
|
+
* source view unchanged.
|
|
387
|
+
*/
|
|
388
|
+
async cp({
|
|
389
|
+
runId,
|
|
390
|
+
turn = 0,
|
|
391
|
+
from,
|
|
392
|
+
to,
|
|
393
|
+
visibility,
|
|
394
|
+
attributes,
|
|
395
|
+
loopId,
|
|
396
|
+
writer,
|
|
397
|
+
}) {
|
|
398
|
+
if (!runId) throw new Error("cp: runId is required");
|
|
399
|
+
if (!from || !to) throw new Error("cp: from and to are required");
|
|
400
|
+
const sourceBody = await this.getBody(runId, from);
|
|
401
|
+
if (sourceBody === null) return;
|
|
402
|
+
await this.set({
|
|
403
|
+
runId,
|
|
404
|
+
turn,
|
|
405
|
+
path: to,
|
|
406
|
+
body: sourceBody,
|
|
407
|
+
visibility,
|
|
408
|
+
attributes,
|
|
409
|
+
loopId,
|
|
410
|
+
writer,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* mv — rename an entry. Equivalent to cp + rm on source.
|
|
416
|
+
*/
|
|
417
|
+
async mv({
|
|
418
|
+
runId,
|
|
419
|
+
turn = 0,
|
|
420
|
+
from,
|
|
421
|
+
to,
|
|
422
|
+
visibility,
|
|
423
|
+
attributes,
|
|
424
|
+
loopId,
|
|
425
|
+
writer,
|
|
426
|
+
}) {
|
|
427
|
+
if (!runId) throw new Error("mv: runId is required");
|
|
428
|
+
if (!from || !to) throw new Error("mv: from and to are required");
|
|
429
|
+
await this.cp({
|
|
430
|
+
runId,
|
|
431
|
+
turn,
|
|
432
|
+
from,
|
|
433
|
+
to,
|
|
434
|
+
visibility,
|
|
435
|
+
attributes,
|
|
436
|
+
loopId,
|
|
437
|
+
writer,
|
|
438
|
+
});
|
|
439
|
+
await this.rm({ runId, path: from });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* update — once-per-turn lifecycle signal from the model (or plugin
|
|
444
|
+
* speaking on its behalf). Writes to update://<slug> with body as the
|
|
445
|
+
* content and attributes.status carrying the model's continuation code
|
|
446
|
+
* (102 continue, 200/204 terminal, 422 can't-answer). Returns the
|
|
447
|
+
* slug path.
|
|
448
|
+
*/
|
|
449
|
+
async update({
|
|
450
|
+
runId,
|
|
451
|
+
turn = 0,
|
|
452
|
+
body,
|
|
453
|
+
status = 102,
|
|
454
|
+
attributes = {},
|
|
455
|
+
loopId = null,
|
|
456
|
+
writer = "plugin",
|
|
457
|
+
}) {
|
|
458
|
+
if (!runId) throw new Error("update: runId is required");
|
|
459
|
+
if (body == null) throw new Error("update: body is required");
|
|
460
|
+
const path = await this.logPath(runId, turn, "update", body);
|
|
461
|
+
await this.set({
|
|
462
|
+
runId,
|
|
463
|
+
turn,
|
|
464
|
+
path,
|
|
465
|
+
body,
|
|
466
|
+
state: "resolved",
|
|
467
|
+
loopId,
|
|
468
|
+
writer,
|
|
469
|
+
attributes: { status, ...attributes },
|
|
470
|
+
});
|
|
471
|
+
return path;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async getEntriesByPattern(
|
|
475
|
+
runId,
|
|
476
|
+
path,
|
|
477
|
+
body = null,
|
|
478
|
+
{ limit = null, offset = null, includeAuditSchemes = false } = {},
|
|
479
|
+
) {
|
|
480
|
+
return this.#db.get_entries_by_pattern.all({
|
|
481
|
+
run_id: runId,
|
|
482
|
+
path,
|
|
483
|
+
body: body ? body : null,
|
|
484
|
+
limit,
|
|
485
|
+
offset,
|
|
486
|
+
include_audit_schemes: includeAuditSchemes ? 1 : null,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#drainPendingResolution(runId, normalized) {
|
|
491
|
+
const key = `${runId}:${normalized}`;
|
|
492
|
+
const resolver = this.#pendingResolutions.get(key);
|
|
493
|
+
if (resolver) {
|
|
494
|
+
this.#pendingResolutions.delete(key);
|
|
495
|
+
resolver();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async waitForResolution(runId, path) {
|
|
500
|
+
// Check current state first — if a synchronous in-process resolver
|
|
501
|
+
// (yolo) flipped the entry to terminal during proposal.pending,
|
|
502
|
+
// the state change has already happened and no future drain will
|
|
503
|
+
// fire. Without this guard, in-process resolvers would deadlock.
|
|
504
|
+
const current = await this.getState(runId, path);
|
|
505
|
+
if (
|
|
506
|
+
current &&
|
|
507
|
+
current.state !== "proposed" &&
|
|
508
|
+
current.state !== "streaming"
|
|
509
|
+
) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const normalized = Entries.normalizePath(path);
|
|
513
|
+
const key = `${runId}:${normalized}`;
|
|
514
|
+
return new Promise((resolve) => {
|
|
515
|
+
this.#pendingResolutions.set(key, resolve);
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async getLog(runId) {
|
|
520
|
+
return this.#db.get_results.all({ run_id: runId });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async getEntries(runId) {
|
|
524
|
+
return this.#db.get_known_entries.all({ run_id: runId });
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async getFileEntries(runId) {
|
|
528
|
+
return this.#db.get_file_entries.all({ run_id: runId });
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async getFileStatesByPattern(runId, pattern) {
|
|
532
|
+
return this.#db.get_file_states_by_pattern.all({ run_id: runId, pattern });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async hasRejections(runId, loopId) {
|
|
536
|
+
const row = await this.#db.has_rejections.get({
|
|
537
|
+
run_id: runId,
|
|
538
|
+
loop_id: loopId,
|
|
539
|
+
});
|
|
540
|
+
return row.count > 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async hasAcceptedActions(runId) {
|
|
544
|
+
const row = await this.#db.has_accepted_actions.get({ run_id: runId });
|
|
545
|
+
return row.count > 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async getUnresolved(runId) {
|
|
549
|
+
return this.#db.get_unresolved.all({ run_id: runId });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async countUnknowns(runId) {
|
|
553
|
+
const row = await this.#db.count_unknowns.get({ run_id: runId });
|
|
554
|
+
return row.count;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async getUnknownValues(runId) {
|
|
558
|
+
const rows = await this.#db.get_unknown_values.all({ run_id: runId });
|
|
559
|
+
return new Set(rows.map((r) => r.body));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Unknown entries for a run, in DB order. Rows include path + body.
|
|
564
|
+
*/
|
|
565
|
+
async getUnknowns(runId) {
|
|
566
|
+
return this.#db.get_unknowns.all({ run_id: runId });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async forkEntries(parentRunId, childRunId) {
|
|
570
|
+
await this.#db.fork_known_entries.run({
|
|
571
|
+
new_run_id: childRunId,
|
|
572
|
+
parent_run_id: parentRunId,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async archivePriorPromptArtifacts(runId, currentTurn) {
|
|
577
|
+
await this.#db.archive_prior_prompt_artifacts.run({
|
|
578
|
+
run_id: runId,
|
|
579
|
+
current_turn: currentTurn,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Demote all promoted entries for a run on a given turn. Returns the
|
|
585
|
+
* affected rows (path, tokens) so callers can summarize.
|
|
586
|
+
*
|
|
587
|
+
* Implemented as SELECT-then-UPDATE because SQLite's RETURNING doesn't
|
|
588
|
+
* support the cross-table lookup needed to report content paths/tokens
|
|
589
|
+
* from the view-layer update.
|
|
590
|
+
*/
|
|
591
|
+
async demoteTurnEntries(runId, turn) {
|
|
592
|
+
const targets = await this.#db.get_turn_demotion_targets.all({
|
|
593
|
+
run_id: runId,
|
|
594
|
+
turn,
|
|
595
|
+
});
|
|
596
|
+
await this.#db.demote_turn_entries.run({ run_id: runId, turn });
|
|
597
|
+
return targets;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Demote every currently-visible entry in a run. Used by budget
|
|
602
|
+
* postDispatch as the fallback when this-turn demotion finds nothing
|
|
603
|
+
* and the packet still overflows — left-over promotions from prior
|
|
604
|
+
* turns the model didn't demote themselves. Returns the affected
|
|
605
|
+
* rows (path, tokens, turn) ordered oldest promotion first so the
|
|
606
|
+
* error body can name them.
|
|
607
|
+
*/
|
|
608
|
+
async demoteRunVisibleEntries(runId) {
|
|
609
|
+
const targets = await this.#db.get_run_visible_targets.all({
|
|
610
|
+
run_id: runId,
|
|
611
|
+
});
|
|
612
|
+
await this.#db.demote_run_visible.run({ run_id: runId });
|
|
613
|
+
return targets;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Run metadata lookup. Exposed here so plugins don't reach into
|
|
618
|
+
* core.db for run-scoped lookups.
|
|
619
|
+
*/
|
|
620
|
+
async getRun(runId) {
|
|
621
|
+
return this.#db.get_run_by_id.get({ id: runId });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Turn-level usage stats write (telemetry). Same rationale as getRun.
|
|
626
|
+
*/
|
|
627
|
+
async updateTurnStats(stats) {
|
|
628
|
+
return this.#db.update_turn_stats.run(stats);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async getBody(runId, path) {
|
|
632
|
+
const row = await this.#db.get_entry_body.get({
|
|
633
|
+
run_id: runId,
|
|
634
|
+
path: Entries.normalizePath(path),
|
|
635
|
+
});
|
|
636
|
+
if (!row) return null;
|
|
637
|
+
return row.body;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async setAttributes(runId, path, attrs) {
|
|
641
|
+
const normalized = Entries.normalizePath(path);
|
|
642
|
+
await this.#db.update_entry_attributes.run({
|
|
643
|
+
run_id: runId,
|
|
644
|
+
path: normalized,
|
|
645
|
+
attributes: JSON.stringify(attrs),
|
|
646
|
+
});
|
|
647
|
+
this.#emitChanged(runId, normalized, "attributes");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async getState(runId, path) {
|
|
651
|
+
return this.#db.get_entry_state.get({
|
|
652
|
+
run_id: runId,
|
|
653
|
+
path: Entries.normalizePath(path),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async getAttributes(runId, path) {
|
|
658
|
+
const row = await this.#db.get_entry_attributes.get({
|
|
659
|
+
run_id: runId,
|
|
660
|
+
path: Entries.normalizePath(path),
|
|
661
|
+
});
|
|
662
|
+
return row?.attributes ? JSON.parse(row.attributes) : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async getTurnAudit(runId, turn) {
|
|
666
|
+
return this.#db.get_turn_audit.all({ run_id: runId, turn });
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
static toolFromPath(path) {
|
|
670
|
+
return Entries.scheme(path);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
static isSystemPath(path) {
|
|
674
|
+
return path.includes("://");
|
|
675
|
+
}
|
|
676
|
+
}
|