@possumtech/rummy 0.3.1 → 0.4.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 +11 -0
- package/README.md +5 -1
- package/SPEC.md +31 -17
- package/migrations/001_initial_schema.sql +2 -3
- package/package.json +1 -1
- package/src/agent/AgentLoop.js +50 -151
- package/src/agent/KnownStore.js +15 -7
- package/src/agent/TurnExecutor.js +75 -318
- package/src/agent/XmlParser.js +25 -4
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +11 -61
- package/src/agent/runs.sql +2 -2
- package/src/hooks/Hooks.js +1 -0
- package/src/hooks/ToolRegistry.js +6 -5
- package/src/plugins/ask_user/ask_userDoc.js +3 -8
- package/src/plugins/budget/README.md +26 -18
- package/src/plugins/budget/budget.js +60 -3
- package/src/plugins/budget/recovery.js +47 -0
- package/src/plugins/cp/cpDoc.js +4 -9
- package/src/plugins/env/envDoc.js +3 -8
- package/src/plugins/get/get.js +2 -4
- package/src/plugins/get/getDoc.js +11 -18
- package/src/plugins/helpers.js +2 -2
- package/src/plugins/instructions/instructions.js +3 -2
- package/src/plugins/instructions/preamble.md +27 -16
- package/src/plugins/known/known.js +63 -8
- package/src/plugins/known/knownDoc.js +10 -14
- package/src/plugins/mv/mvDoc.js +6 -21
- package/src/plugins/policy/policy.js +47 -0
- package/src/plugins/progress/progress.js +9 -45
- package/src/plugins/prompt/prompt.js +10 -1
- package/src/plugins/rm/rmDoc.js +5 -10
- package/src/plugins/rpc/rpc.js +3 -1
- package/src/plugins/set/set.js +82 -85
- package/src/plugins/set/setDoc.js +28 -41
- package/src/plugins/sh/shDoc.js +2 -7
- package/src/plugins/summarize/summarize.js +7 -0
- package/src/plugins/summarize/summarizeDoc.js +6 -11
- package/src/plugins/think/think.js +12 -0
- package/src/plugins/think/thinkDoc.js +18 -0
- package/src/plugins/unknown/unknown.js +21 -0
- package/src/plugins/unknown/unknownDoc.js +9 -14
- package/src/plugins/update/update.js +7 -0
- package/src/plugins/update/updateDoc.js +6 -11
- package/src/server/ClientConnection.js +11 -1
- package/src/sql/v_model_context.sql +4 -4
package/.env.example
CHANGED
|
@@ -17,6 +17,7 @@ RUMMY_MMAP_MB=0
|
|
|
17
17
|
|
|
18
18
|
# Agent Loop Limits
|
|
19
19
|
RUMMY_MAX_TURNS=99
|
|
20
|
+
RUMMY_MAX_COMMANDS=15
|
|
20
21
|
RUMMY_MAX_UNKNOWN_WARNINGS=3
|
|
21
22
|
RUMMY_MAX_STALLS=3
|
|
22
23
|
RUMMY_MIN_CYCLES=3
|
|
@@ -34,6 +35,16 @@ RUMMY_FETCH_TIMEOUT=300000
|
|
|
34
35
|
# Debug
|
|
35
36
|
# RUMMY_DEBUG=true
|
|
36
37
|
|
|
38
|
+
# Think tag: 1 = model uses <think> tags for reasoning (default)
|
|
39
|
+
# 0 = disabled, model reasons via API reasoning_content field only
|
|
40
|
+
RUMMY_THINK=1
|
|
41
|
+
|
|
42
|
+
# Budget
|
|
43
|
+
# Fraction of context window used as ceiling. 0.9 = 90%, 10% reserved as headroom.
|
|
44
|
+
RUMMY_BUDGET_CEILING=0.9
|
|
45
|
+
# Maximum tokens per known entry. Entries exceeding this are rejected with 413.
|
|
46
|
+
RUMMY_MAX_ENTRY_TOKENS=512
|
|
47
|
+
|
|
37
48
|
# Token Estimation
|
|
38
49
|
# Characters per token. Lower = more conservative (fewer tokens per character).
|
|
39
50
|
# Default 2. Set to 1 for worst-case (1 token per character).
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RUMMY: Relational Unknowns Memory Management Yoke
|
|
2
2
|
|
|
3
|
-
Rummy is the only LLM agent service inspired by and dedicated to the memory of former Secretary of
|
|
3
|
+
Rummy is the only LLM agent service inspired by and dedicated to the memory of former Secretary of Defense Donald "Rummy" Rumsfeld. Our unique fusion of apophatic and hedbergian engineering strategies yields more accurate and efficient results than any other agent. Our client/server and plugin architecture integrates it into more workflows than any other agent. It's also more flexible and lean than any other agent. Our dynamic cache management, model hot-swapping, and flexible router interface make it more affordable than any other agent.
|
|
4
4
|
|
|
5
5
|
## Key Features
|
|
6
6
|
|
|
@@ -10,6 +10,10 @@ Rummy is the only LLM agent service inspired by and dedicated to the memory of f
|
|
|
10
10
|
|
|
11
11
|
- **Hedberg:** The interpretation boundary between stochastic model output and deterministic system operations. Models speak in whatever syntax they were trained on — sed regex, SEARCH/REPLACE blocks, escaped characters. Hedberg normalizes all of it. Available to all plugins via `core.hooks.hedberg`.
|
|
12
12
|
|
|
13
|
+
- **Folksonomic Memory:** The model organizes its own knowledge into navigable path hierarchies with searchable summary tags. Not RAG — the model builds and curates its own taxonomy using `<known>` entries with paths like `known://project/architecture`.
|
|
14
|
+
|
|
15
|
+
- **Fidelity System:** Every entry has a visibility level: full, summary, index, archive. The model manages its own context by promoting what it needs and demoting what it doesn't. Budget enforcement catches overflow post-dispatch — tools run uninterrupted, demotion happens after.
|
|
16
|
+
|
|
13
17
|
- **Plugin Architecture:** Every `<tag>` the model sees is a plugin. Every scheme is registered by its owner. The prompt itself is assembled from plugins. Drop a directory into `~/.rummy/plugins/` or install via npm. See [PLUGINS.md](PLUGINS.md) for the complete plugin API.
|
|
14
18
|
|
|
15
19
|
- **Symbols Done Right:** Designed with universal language support in mind. Powered by [@possumtech/antlrmap](https://github.com/possumtech/antlrmap).
|
package/SPEC.md
CHANGED
|
@@ -44,7 +44,7 @@ body, attributes, and state.
|
|
|
44
44
|
known_entries (
|
|
45
45
|
id, run_id, loop_id, turn, path, body, scheme,
|
|
46
46
|
status INTEGER, fidelity TEXT, hash,
|
|
47
|
-
attributes, tokens,
|
|
47
|
+
attributes, tokens, refs, write_count,
|
|
48
48
|
created_at, updated_at
|
|
49
49
|
)
|
|
50
50
|
```
|
|
@@ -56,10 +56,9 @@ known_entries (
|
|
|
56
56
|
| `attributes` | Tag attributes as JSON. Handler-private workspace. `CHECK (json_valid)` |
|
|
57
57
|
| `scheme` | Generated from path via `schemeOf()`. Drives dispatch and view routing |
|
|
58
58
|
| `status` | HTTP status code (200, 202, 400, 413, etc.) |
|
|
59
|
-
| `fidelity` | Visibility level: full, summary,
|
|
59
|
+
| `fidelity` | Visibility level: full, summary, archive |
|
|
60
60
|
| `hash` | SHA-256 for file change detection |
|
|
61
|
-
| `tokens` |
|
|
62
|
-
| `tokens_full` | Cost of raw body at full fidelity |
|
|
61
|
+
| `tokens` | Full-body token cost. Never changes on demotion/promotion. |
|
|
63
62
|
| `turn` | Freshness — when was this entry last touched |
|
|
64
63
|
|
|
65
64
|
### 1.2 Schemes, Status & Fidelity
|
|
@@ -211,8 +210,8 @@ object is the same shape at every tier.
|
|
|
211
210
|
Model tier restrictions enforced by unified `resolveForLoop(mode, flags)`.
|
|
212
211
|
Ask mode excludes `sh`. Flags: `noInteraction` excludes `ask_user`,
|
|
213
212
|
`noWeb` excludes `search`, `noProposals` excludes `ask_user`/`env`/`sh`.
|
|
214
|
-
|
|
215
|
-
|
|
213
|
+
14 model tools: think, unknown, known, get, set, env, sh, rm, cp, mv,
|
|
214
|
+
ask_user, update, summarize, search.
|
|
216
215
|
Client tier requires project init. Plugin tier has no restrictions.
|
|
217
216
|
|
|
218
217
|
### 3.2 Dispatch Path
|
|
@@ -225,13 +224,28 @@ Client: JSON-RPC → { method, params } → #record() → dispatch(scheme, en
|
|
|
225
224
|
Plugin: rummy.rm({ path }) → #record() → dispatch(scheme, entry, rummy)
|
|
226
225
|
```
|
|
227
226
|
|
|
228
|
-
**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
227
|
+
**Tool dispatch:** Commands are dispatched sequentially in the order
|
|
228
|
+
the model emitted them. Each tool either succeeds (200), fails (400+),
|
|
229
|
+
or proposes (202). On failure, all remaining tools are aborted. On
|
|
230
|
+
proposal, dispatch pauses, a notification is pushed to the client
|
|
231
|
+
(same WebSocket push pattern as `run/progress`), the client resolves
|
|
232
|
+
(accept/reject), and dispatch resumes — the proposal becomes 200 or
|
|
233
|
+
400+ like any other tool. The `ask`/`act` RPC response is only sent
|
|
234
|
+
when all tools have completed. Proposals are NOT batched — each is
|
|
235
|
+
sent and resolved inline during dispatch. The model controls tool
|
|
236
|
+
ordering; the system respects it.
|
|
237
|
+
|
|
238
|
+
If the model sends `<summarize>` but a preceding action in the same
|
|
239
|
+
turn failed, the summarize is overridden to an update (the model's
|
|
240
|
+
assertion that it's done is false). Both `<summarize>` and `<update>`
|
|
241
|
+
present → last signal wins.
|
|
242
|
+
|
|
243
|
+
**Post-dispatch budget check:** After all tools dispatch, the system
|
|
244
|
+
materializes context and checks the budget ceiling. If context exceeds
|
|
245
|
+
the ceiling, Turn Demotion fires — all entries from this turn are
|
|
246
|
+
demoted to summary and a `budget://` entry is written. This is a
|
|
247
|
+
system housekeeping step independent of tool success/failure. The
|
|
248
|
+
tools already ran; their outcomes are settled.
|
|
235
249
|
|
|
236
250
|
### 3.3 Plugin Convention
|
|
237
251
|
|
|
@@ -293,7 +307,7 @@ Two messages per turn. System = stable truth. User = active task.
|
|
|
293
307
|
[skills/]
|
|
294
308
|
[/instructions]
|
|
295
309
|
<knowns>
|
|
296
|
-
...entries sorted by fidelity (
|
|
310
|
+
...entries sorted by fidelity (summary, full), then by scheme
|
|
297
311
|
</knowns>
|
|
298
312
|
<previous>
|
|
299
313
|
(pre-loop entries, each with turn, status, summary, fidelity, tokens)
|
|
@@ -531,7 +545,7 @@ ask_user. `noRepo: true` — no file scanning during panic.
|
|
|
531
545
|
`budget.panicPrompt()`: the assembled token count, the target, and
|
|
532
546
|
the exact number of tokens to free. Turn 2+ receives a continuation
|
|
533
547
|
prompt. The model uses `<set fidelity="archive">`, `<mv
|
|
534
|
-
fidelity="
|
|
548
|
+
fidelity="summary">`, and similar fidelity operations to free space,
|
|
535
549
|
concluding with `<summarize>` when done or `<update>` while working.
|
|
536
550
|
|
|
537
551
|
---
|
|
@@ -660,7 +674,7 @@ simple to powerful — weak models learn from examples 1-2, strong models
|
|
|
660
674
|
pick up the pattern from example 3.
|
|
661
675
|
|
|
662
676
|
**Lifecycle continuity.** Examples weave stories across tools. The get
|
|
663
|
-
docs end with `<set path="..." fidelity="
|
|
677
|
+
docs end with `<set path="..." fidelity="summary"/>`. The known docs
|
|
664
678
|
reference `<get path="known://*">keyword</get>` for recall and
|
|
665
679
|
`<set path="known://..." archive/>` for archiving. The unknown docs
|
|
666
680
|
reference `<get/>` for investigation and `<rm/>` for cleanup. A model
|
|
@@ -746,7 +760,7 @@ Termination protocol:
|
|
|
746
760
|
- `<summarize>` → run terminates
|
|
747
761
|
- `<summarize>` + failed actions → overridden to `<update>` (continue)
|
|
748
762
|
- `<update>` → run continues
|
|
749
|
-
- Both →
|
|
763
|
+
- Both → last signal wins (respects the model's final intent)
|
|
750
764
|
- Neither + investigation tools → stall counter (RUMMY_MAX_STALLS)
|
|
751
765
|
- Neither + action-only tools → healed to summarize
|
|
752
766
|
- Neither + plain text → healed to summarize
|
|
@@ -125,12 +125,11 @@ CREATE TABLE IF NOT EXISTS known_entries (
|
|
|
125
125
|
, scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
|
|
126
126
|
, status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
|
|
127
127
|
, fidelity TEXT NOT NULL DEFAULT 'full' CHECK (
|
|
128
|
-
fidelity IN ('full', 'summary', '
|
|
128
|
+
fidelity IN ('full', 'summary', 'archive')
|
|
129
129
|
)
|
|
130
130
|
, hash TEXT
|
|
131
131
|
, attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
|
|
132
132
|
, tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
|
|
133
|
-
, tokens_full INTEGER NOT NULL DEFAULT 0 CHECK (tokens_full >= 0)
|
|
134
133
|
, refs INTEGER NOT NULL DEFAULT 0 CHECK (refs >= 0)
|
|
135
134
|
, write_count INTEGER NOT NULL DEFAULT 1 CHECK (write_count >= 1)
|
|
136
135
|
, created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
@@ -167,7 +166,7 @@ CREATE TABLE IF NOT EXISTS turn_context (
|
|
|
167
166
|
, path TEXT NOT NULL
|
|
168
167
|
, scheme TEXT GENERATED ALWAYS AS (schemeOf(path)) STORED
|
|
169
168
|
, status INTEGER NOT NULL DEFAULT 200 CHECK (status BETWEEN 100 AND 599)
|
|
170
|
-
, fidelity TEXT NOT NULL CHECK (fidelity IN ('full', 'summary'
|
|
169
|
+
, fidelity TEXT NOT NULL CHECK (fidelity IN ('full', 'summary'))
|
|
171
170
|
, body TEXT NOT NULL DEFAULT ''
|
|
172
171
|
, tokens INTEGER NOT NULL DEFAULT 0 CHECK (tokens >= 0)
|
|
173
172
|
, attributes JSON NOT NULL DEFAULT '{}' CHECK (json_valid(attributes))
|
package/package.json
CHANGED
package/src/agent/AgentLoop.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { advanceRecovery } from "../plugins/budget/recovery.js";
|
|
1
2
|
import KnownStore from "./KnownStore.js";
|
|
2
3
|
import msg from "./messages.js";
|
|
3
4
|
import ResponseHealer from "./ResponseHealer.js";
|
|
@@ -70,14 +71,15 @@ export default class AgentLoop {
|
|
|
70
71
|
const existing = this.#activeRuns.get(existingRun.id);
|
|
71
72
|
if (existing) existing.abort();
|
|
72
73
|
|
|
74
|
+
// Clean up stale proposals from interrupted runs
|
|
73
75
|
const unresolved = await this.#knownStore.getUnresolved(existingRun.id);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
76
|
+
for (const u of unresolved) {
|
|
77
|
+
await this.#knownStore.resolve(
|
|
78
|
+
existingRun.id,
|
|
79
|
+
u.path,
|
|
80
|
+
499,
|
|
81
|
+
"Stale proposal from interrupted run",
|
|
82
|
+
);
|
|
81
83
|
}
|
|
82
84
|
return { runId: existingRun.id, alias: existingRun.alias };
|
|
83
85
|
}
|
|
@@ -125,15 +127,6 @@ export default class AgentLoop {
|
|
|
125
127
|
const requestedModel = model;
|
|
126
128
|
|
|
127
129
|
const runInfo = await this.#ensureRun(projectId, model, run, options);
|
|
128
|
-
if (runInfo.blocked) {
|
|
129
|
-
return {
|
|
130
|
-
run: runInfo.alias,
|
|
131
|
-
status: 202,
|
|
132
|
-
remainingCount: runInfo.proposed.length,
|
|
133
|
-
proposed: runInfo.proposed,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
130
|
const { runId: currentRunId, alias: currentAlias } = runInfo;
|
|
138
131
|
|
|
139
132
|
const loopSeq = await this.#db.next_loop.get({ run_id: currentRunId });
|
|
@@ -222,11 +215,9 @@ export default class AgentLoop {
|
|
|
222
215
|
|
|
223
216
|
await this.#db.complete_loop.run({
|
|
224
217
|
id: loop.id,
|
|
225
|
-
status: result.status
|
|
218
|
+
status: result.status,
|
|
226
219
|
result: JSON.stringify(result),
|
|
227
220
|
});
|
|
228
|
-
|
|
229
|
-
if (result.status === 202) return result;
|
|
230
221
|
}
|
|
231
222
|
|
|
232
223
|
const runRow = await this.#db.get_run_by_alias.get({
|
|
@@ -282,12 +273,9 @@ export default class AgentLoop {
|
|
|
282
273
|
let _lastAssembledTokens = 0;
|
|
283
274
|
let recovery = null; // { target, promptPath, strikes, lastTokens }
|
|
284
275
|
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
currentRunId,
|
|
289
|
-
currentLoopId,
|
|
290
|
-
);
|
|
276
|
+
// Previous loop entries stay at full fidelity — the model is
|
|
277
|
+
// instructed to summarize and demote them. Budget enforcement
|
|
278
|
+
// catches overflow if the model fails to manage context.
|
|
291
279
|
|
|
292
280
|
// Restore any prompt entries left at summary fidelity by a recovery
|
|
293
281
|
// phase that was interrupted (server crash, restart). If the full
|
|
@@ -347,7 +335,16 @@ export default class AgentLoop {
|
|
|
347
335
|
});
|
|
348
336
|
|
|
349
337
|
if (result.status === 413) {
|
|
350
|
-
|
|
338
|
+
await this.#db.complete_loop.run({
|
|
339
|
+
id: currentLoopId,
|
|
340
|
+
status: 413,
|
|
341
|
+
result: null,
|
|
342
|
+
});
|
|
343
|
+
await this.#db.update_run_status.run({
|
|
344
|
+
id: currentRunId,
|
|
345
|
+
status: 200,
|
|
346
|
+
});
|
|
347
|
+
const out = {
|
|
351
348
|
run: currentAlias,
|
|
352
349
|
status: 413,
|
|
353
350
|
overflow: result.overflow,
|
|
@@ -355,6 +352,8 @@ export default class AgentLoop {
|
|
|
355
352
|
contextSize: result.contextSize,
|
|
356
353
|
turn: result.turn,
|
|
357
354
|
};
|
|
355
|
+
await hook.completed.emit({ projectId, ...out });
|
|
356
|
+
return out;
|
|
358
357
|
}
|
|
359
358
|
|
|
360
359
|
_lastAssembledTokens = result.assembledTokens;
|
|
@@ -390,8 +389,6 @@ export default class AgentLoop {
|
|
|
390
389
|
const unknowns = await this.#db.get_unknowns.all({
|
|
391
390
|
run_id: currentRunId,
|
|
392
391
|
});
|
|
393
|
-
const unresolved = await this.#knownStore.getUnresolved(currentRunId);
|
|
394
|
-
|
|
395
392
|
const latestSummary = history
|
|
396
393
|
.filter((e) => e.status === 200 && e.path?.startsWith("summarize://"))
|
|
397
394
|
.at(-1);
|
|
@@ -400,15 +397,10 @@ export default class AgentLoop {
|
|
|
400
397
|
projectId,
|
|
401
398
|
run: currentAlias,
|
|
402
399
|
turn: result.turn,
|
|
403
|
-
status:
|
|
400
|
+
status: 102,
|
|
404
401
|
summary: latestSummary?.body || "",
|
|
405
402
|
history,
|
|
406
403
|
unknowns: unknowns.map((u) => ({ path: u.path, body: u.body })),
|
|
407
|
-
proposed: unresolved.map((p) => ({
|
|
408
|
-
path: p.path,
|
|
409
|
-
type: KnownStore.toolFromPath(p.path) || "unknown",
|
|
410
|
-
attributes: p.attributes ? JSON.parse(p.attributes) : null,
|
|
411
|
-
})),
|
|
412
404
|
telemetry: {
|
|
413
405
|
modelAlias: result.modelAlias,
|
|
414
406
|
model: result.model,
|
|
@@ -433,21 +425,6 @@ export default class AgentLoop {
|
|
|
433
425
|
}),
|
|
434
426
|
},
|
|
435
427
|
});
|
|
436
|
-
if (unresolved.length > 0) {
|
|
437
|
-
await this.#db.update_run_status.run({
|
|
438
|
-
id: currentRunId,
|
|
439
|
-
status: 202,
|
|
440
|
-
});
|
|
441
|
-
const out = {
|
|
442
|
-
run: currentAlias,
|
|
443
|
-
status: 202,
|
|
444
|
-
turn: result.turn,
|
|
445
|
-
proposed: unresolved,
|
|
446
|
-
};
|
|
447
|
-
await hook.completed.emit({ projectId, ...out });
|
|
448
|
-
return out;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
428
|
await this.#hooks.run.step.completed.emit({
|
|
452
429
|
projectId,
|
|
453
430
|
run: currentAlias,
|
|
@@ -574,6 +551,12 @@ export default class AgentLoop {
|
|
|
574
551
|
}
|
|
575
552
|
|
|
576
553
|
if (action === "accept") {
|
|
554
|
+
const projectId = runRow.project_id;
|
|
555
|
+
const project = await this.#db.get_project_by_id.get({
|
|
556
|
+
id: projectId,
|
|
557
|
+
});
|
|
558
|
+
const projectRoot = project?.project_root;
|
|
559
|
+
|
|
577
560
|
if (path.startsWith("set://") && attrs?.file && attrs?.merge) {
|
|
578
561
|
const fileBody = await this.#knownStore.getBody(runId, attrs.file);
|
|
579
562
|
if (fileBody != null) {
|
|
@@ -594,12 +577,25 @@ export default class AgentLoop {
|
|
|
594
577
|
patched,
|
|
595
578
|
200,
|
|
596
579
|
);
|
|
580
|
+
// Write patched content to disk
|
|
581
|
+
if (projectRoot) {
|
|
582
|
+
const { writeFile } = await import("node:fs/promises");
|
|
583
|
+
const { join } = await import("node:path");
|
|
584
|
+
await writeFile(join(projectRoot, attrs.file), patched).catch(
|
|
585
|
+
() => {},
|
|
586
|
+
);
|
|
587
|
+
}
|
|
597
588
|
}
|
|
598
589
|
}
|
|
599
590
|
|
|
600
591
|
if (path.startsWith("rm://")) {
|
|
601
592
|
if (attrs?.path) {
|
|
602
593
|
await this.#knownStore.remove(runId, attrs.path);
|
|
594
|
+
if (projectRoot) {
|
|
595
|
+
const { unlink } = await import("node:fs/promises");
|
|
596
|
+
const { join } = await import("node:path");
|
|
597
|
+
await unlink(join(projectRoot, attrs.path)).catch(() => {});
|
|
598
|
+
}
|
|
603
599
|
}
|
|
604
600
|
}
|
|
605
601
|
|
|
@@ -615,68 +611,9 @@ export default class AgentLoop {
|
|
|
615
611
|
throw new Error(msg("error.resolution_invalid", { action }));
|
|
616
612
|
}
|
|
617
613
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
run: runAlias,
|
|
622
|
-
status: 202,
|
|
623
|
-
remainingCount: unresolved.length,
|
|
624
|
-
proposed: unresolved,
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Scope completion checks to the current loop
|
|
629
|
-
const currentLoop = await this.#db.get_current_loop.get({ run_id: runId });
|
|
630
|
-
const loopId = currentLoop?.id ?? null;
|
|
631
|
-
|
|
632
|
-
if (await this.#knownStore.hasRejections(runId, loopId)) {
|
|
633
|
-
if (currentLoop)
|
|
634
|
-
await this.#db.complete_loop.run({
|
|
635
|
-
id: loopId,
|
|
636
|
-
status: 200,
|
|
637
|
-
result: null,
|
|
638
|
-
});
|
|
639
|
-
await this.#db.update_run_status.run({ id: runId, status: 200 });
|
|
640
|
-
return { run: runAlias, status: 200 };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const hasSummary = await this.#db.get_latest_summary.get({
|
|
644
|
-
run_id: runId,
|
|
645
|
-
loop_id: loopId,
|
|
646
|
-
});
|
|
647
|
-
if (hasSummary?.body) {
|
|
648
|
-
if (currentLoop)
|
|
649
|
-
await this.#db.complete_loop.run({
|
|
650
|
-
id: loopId,
|
|
651
|
-
status: 200,
|
|
652
|
-
result: null,
|
|
653
|
-
});
|
|
654
|
-
await this.#db.update_run_status.run({ id: runId, status: 200 });
|
|
655
|
-
return { run: runAlias, status: 200 };
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// No summary and no rejections in this loop — resume it
|
|
659
|
-
const projectId = runRow.project_id;
|
|
660
|
-
const project = await this.#db.get_project_by_id.get({ id: projectId });
|
|
661
|
-
|
|
662
|
-
const latestPrompt = await this.#db.get_latest_prompt.get({
|
|
663
|
-
run_id: runId,
|
|
664
|
-
});
|
|
665
|
-
const resumeMode = latestPrompt?.attributes
|
|
666
|
-
? JSON.parse(latestPrompt.attributes).mode
|
|
667
|
-
: "ask";
|
|
668
|
-
|
|
669
|
-
// Re-enqueue the current loop's prompt to continue it
|
|
670
|
-
const loopSeq = await this.#db.next_loop.get({ run_id: runId });
|
|
671
|
-
await this.#db.enqueue_loop.get({
|
|
672
|
-
run_id: runId,
|
|
673
|
-
sequence: loopSeq.sequence,
|
|
674
|
-
mode: resumeMode,
|
|
675
|
-
model: runRow.model,
|
|
676
|
-
prompt: "",
|
|
677
|
-
config: currentLoop?.config || "{}",
|
|
678
|
-
});
|
|
679
|
-
return this.#drainQueue(runId, runAlias, projectId, project, {});
|
|
614
|
+
// The dispatch loop is awaiting resolution. This unblocks it.
|
|
615
|
+
// Dispatch continuation is handled by the loop, not here.
|
|
616
|
+
return { run: runAlias, status: 200 };
|
|
680
617
|
}
|
|
681
618
|
|
|
682
619
|
async #composeResolvedContent(runId, path, _attrs, output) {
|
|
@@ -741,43 +678,5 @@ export default class AgentLoop {
|
|
|
741
678
|
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
742
679
|
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
743
680
|
*/
|
|
744
|
-
export
|
|
745
|
-
|
|
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
|
-
}
|
|
681
|
+
// Re-export for backward compatibility with tests
|
|
682
|
+
export { advanceRecovery } from "../plugins/budget/recovery.js";
|
package/src/agent/KnownStore.js
CHANGED
|
@@ -5,6 +5,7 @@ export default class KnownStore {
|
|
|
5
5
|
#onChanged;
|
|
6
6
|
#schemes = new Map();
|
|
7
7
|
#seq = 0;
|
|
8
|
+
#pendingResolutions = new Map();
|
|
8
9
|
|
|
9
10
|
constructor(db, { onChanged } = {}) {
|
|
10
11
|
this.#db = db;
|
|
@@ -225,6 +226,20 @@ export default class KnownStore {
|
|
|
225
226
|
body,
|
|
226
227
|
});
|
|
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
|
+
});
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
async restoreSummarizedPrompts(runId) {
|
|
@@ -232,13 +247,6 @@ export default class KnownStore {
|
|
|
232
247
|
this.#emitChanged(runId, "prompt://batch", "fidelity");
|
|
233
248
|
}
|
|
234
249
|
|
|
235
|
-
async demotePreviousLoopLogging(runId, loopId) {
|
|
236
|
-
await this.#db.demote_previous_loop_logging.run({
|
|
237
|
-
run_id: runId,
|
|
238
|
-
loop_id: loopId,
|
|
239
|
-
});
|
|
240
|
-
this.#emitChanged(runId, "logging://batch", "fidelity");
|
|
241
|
-
}
|
|
242
250
|
|
|
243
251
|
async getLog(runId) {
|
|
244
252
|
return this.#db.get_results.all({ run_id: runId });
|