@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
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
-- PREP: upsert_known_entry
|
|
2
2
|
INSERT INTO known_entries (
|
|
3
3
|
run_id, loop_id, turn, path, body, status, fidelity, hash
|
|
4
|
-
, attributes, tokens,
|
|
4
|
+
, attributes, tokens, updated_at
|
|
5
5
|
)
|
|
6
6
|
VALUES (
|
|
7
7
|
:run_id, :loop_id, :turn, :path, :body, :status, :fidelity, :hash
|
|
8
8
|
, COALESCE(:attributes, '{}')
|
|
9
9
|
, countTokens(:body)
|
|
10
|
-
, countTokens(:body)
|
|
11
10
|
, COALESCE(:updated_at, CURRENT_TIMESTAMP)
|
|
12
11
|
)
|
|
13
12
|
ON CONFLICT (run_id, path) DO UPDATE SET
|
|
@@ -19,13 +18,12 @@ ON CONFLICT (run_id, path) DO UPDATE SET
|
|
|
19
18
|
, loop_id = excluded.loop_id
|
|
20
19
|
, turn = excluded.turn
|
|
21
20
|
, tokens = countTokens(excluded.body)
|
|
22
|
-
, tokens_full = countTokens(excluded.body)
|
|
23
21
|
, write_count = known_entries.write_count + 1
|
|
24
22
|
, updated_at = COALESCE(excluded.updated_at, CURRENT_TIMESTAMP);
|
|
25
23
|
|
|
26
24
|
-- PREP: recount_tokens
|
|
27
25
|
UPDATE known_entries
|
|
28
|
-
SET tokens = :tokens
|
|
26
|
+
SET tokens = :tokens
|
|
29
27
|
WHERE run_id = :run_id AND path = :path;
|
|
30
28
|
|
|
31
29
|
-- PREP: get_stale_tokens
|
|
@@ -55,18 +53,6 @@ WHERE run_id = :run_id AND path = :path;
|
|
|
55
53
|
UPDATE known_entries
|
|
56
54
|
SET
|
|
57
55
|
fidelity = :fidelity
|
|
58
|
-
, tokens = CASE
|
|
59
|
-
WHEN :fidelity = 'archive'
|
|
60
|
-
THEN 0
|
|
61
|
-
WHEN :fidelity = 'index'
|
|
62
|
-
THEN 0
|
|
63
|
-
WHEN :fidelity = 'summary'
|
|
64
|
-
THEN COALESCE(
|
|
65
|
-
countTokens(json_extract(attributes, '$.summary')),
|
|
66
|
-
countTokens(substr(body, 1, 80))
|
|
67
|
-
)
|
|
68
|
-
ELSE tokens_full
|
|
69
|
-
END
|
|
70
56
|
, updated_at = CURRENT_TIMESTAMP
|
|
71
57
|
WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
|
|
72
58
|
|
|
@@ -74,8 +60,8 @@ WHERE run_id = :run_id AND hedmatch(:pattern, path) AND scheme IS NULL;
|
|
|
74
60
|
UPDATE known_entries
|
|
75
61
|
SET
|
|
76
62
|
fidelity = 'full'
|
|
63
|
+
, status = 200
|
|
77
64
|
, turn = :turn
|
|
78
|
-
, tokens = tokens_full
|
|
79
65
|
, updated_at = CURRENT_TIMESTAMP
|
|
80
66
|
WHERE run_id = :run_id AND path = :path;
|
|
81
67
|
|
|
@@ -83,26 +69,14 @@ WHERE run_id = :run_id AND path = :path;
|
|
|
83
69
|
UPDATE known_entries
|
|
84
70
|
SET
|
|
85
71
|
fidelity = 'archive'
|
|
86
|
-
, tokens = 0
|
|
87
72
|
, updated_at = CURRENT_TIMESTAMP
|
|
88
73
|
WHERE run_id = :run_id AND path = :path;
|
|
89
74
|
|
|
90
75
|
-- PREP: set_fidelity
|
|
76
|
+
-- Tokens unchanged — always reflects full body cost.
|
|
91
77
|
UPDATE known_entries
|
|
92
78
|
SET
|
|
93
79
|
fidelity = :fidelity
|
|
94
|
-
, tokens = CASE
|
|
95
|
-
WHEN :fidelity = 'archive'
|
|
96
|
-
THEN 0
|
|
97
|
-
WHEN :fidelity = 'index'
|
|
98
|
-
THEN 0
|
|
99
|
-
WHEN :fidelity = 'summary'
|
|
100
|
-
THEN COALESCE(
|
|
101
|
-
countTokens(json_extract(attributes, '$.summary')),
|
|
102
|
-
countTokens(substr(body, 1, 80))
|
|
103
|
-
)
|
|
104
|
-
ELSE countTokens(body)
|
|
105
|
-
END
|
|
106
80
|
, updated_at = CURRENT_TIMESTAMP
|
|
107
81
|
WHERE run_id = :run_id AND path = :path;
|
|
108
82
|
|
|
@@ -138,8 +112,8 @@ WHERE run_id = :run_id AND path = :path;
|
|
|
138
112
|
UPDATE known_entries
|
|
139
113
|
SET
|
|
140
114
|
fidelity = 'full'
|
|
115
|
+
, status = 200
|
|
141
116
|
, turn = :turn
|
|
142
|
-
, tokens = tokens_full
|
|
143
117
|
, updated_at = CURRENT_TIMESTAMP
|
|
144
118
|
WHERE
|
|
145
119
|
run_id = :run_id
|
|
@@ -150,7 +124,6 @@ WHERE
|
|
|
150
124
|
UPDATE known_entries
|
|
151
125
|
SET
|
|
152
126
|
fidelity = 'archive'
|
|
153
|
-
, tokens = 0
|
|
154
127
|
, updated_at = CURRENT_TIMESTAMP
|
|
155
128
|
WHERE
|
|
156
129
|
run_id = :run_id
|
|
@@ -158,7 +131,7 @@ WHERE
|
|
|
158
131
|
AND (:body IS NULL OR hedsearch(:body, body));
|
|
159
132
|
|
|
160
133
|
-- PREP: get_entries_by_pattern
|
|
161
|
-
SELECT path, body, scheme, status, fidelity,
|
|
134
|
+
SELECT path, body, scheme, status, fidelity, tokens, attributes
|
|
162
135
|
FROM known_entries
|
|
163
136
|
WHERE
|
|
164
137
|
run_id = :run_id
|
|
@@ -182,7 +155,6 @@ UPDATE known_entries
|
|
|
182
155
|
SET
|
|
183
156
|
body = :new_body
|
|
184
157
|
, tokens = countTokens(:new_body)
|
|
185
|
-
, tokens_full = countTokens(:new_body)
|
|
186
158
|
, write_count = write_count + 1
|
|
187
159
|
, updated_at = CURRENT_TIMESTAMP
|
|
188
160
|
WHERE
|
|
@@ -197,43 +169,21 @@ WHERE
|
|
|
197
169
|
UPDATE known_entries
|
|
198
170
|
SET
|
|
199
171
|
fidelity = 'full'
|
|
200
|
-
, tokens = tokens_full
|
|
201
172
|
, updated_at = CURRENT_TIMESTAMP
|
|
202
173
|
WHERE run_id = :run_id AND scheme = 'prompt' AND fidelity = 'summary';
|
|
203
174
|
|
|
204
|
-
-- PREP:
|
|
205
|
-
-- Demote full
|
|
206
|
-
--
|
|
207
|
-
UPDATE known_entries
|
|
208
|
-
SET
|
|
209
|
-
fidelity = 'summary'
|
|
210
|
-
, tokens = COALESCE(
|
|
211
|
-
countTokens(json_extract(attributes, '$.summary'))
|
|
212
|
-
, countTokens(substr(body, 1, 80))
|
|
213
|
-
)
|
|
214
|
-
, updated_at = CURRENT_TIMESTAMP
|
|
215
|
-
WHERE
|
|
216
|
-
run_id = :run_id
|
|
217
|
-
AND (loop_id IS NULL OR loop_id != :loop_id)
|
|
218
|
-
AND fidelity = 'full'
|
|
219
|
-
AND scheme IN (SELECT name FROM schemes WHERE category = 'logging');
|
|
220
|
-
|
|
221
|
-
-- PREP: demote_turn_data_entries
|
|
222
|
-
-- Demote full data entries from a turn to summary with 413 status.
|
|
223
|
-
-- Fires when end-of-turn materialization exceeds the context ceiling.
|
|
175
|
+
-- PREP: demote_turn_entries
|
|
176
|
+
-- Demote all full entries from a turn to summary with 413 status.
|
|
177
|
+
-- Tokens unchanged — always reports full cost regardless of fidelity.
|
|
224
178
|
UPDATE known_entries
|
|
225
179
|
SET
|
|
226
180
|
fidelity = 'summary'
|
|
227
181
|
, status = 413
|
|
228
|
-
, tokens = COALESCE(
|
|
229
|
-
countTokens(json_extract(attributes, '$.summary'))
|
|
230
|
-
, countTokens(substr(body, 1, 80))
|
|
231
|
-
)
|
|
232
182
|
, updated_at = CURRENT_TIMESTAMP
|
|
233
183
|
WHERE
|
|
234
184
|
run_id = :run_id
|
|
235
185
|
AND turn = :turn
|
|
236
186
|
AND fidelity = 'full'
|
|
237
187
|
AND status < 400
|
|
238
|
-
|
|
239
|
-
|
|
188
|
+
RETURNING path, tokens;
|
|
189
|
+
|
package/src/agent/runs.sql
CHANGED
|
@@ -81,11 +81,11 @@ RETURNING next_turn - 1 AS turn;
|
|
|
81
81
|
-- PREP: fork_known_entries
|
|
82
82
|
INSERT INTO known_entries (
|
|
83
83
|
run_id, loop_id, turn, path, body, status, fidelity
|
|
84
|
-
, hash, attributes, tokens,
|
|
84
|
+
, hash, attributes, tokens, refs, write_count
|
|
85
85
|
)
|
|
86
86
|
SELECT
|
|
87
87
|
:new_run_id, NULL, turn, path, body, status, fidelity
|
|
88
|
-
, hash, attributes, tokens,
|
|
88
|
+
, hash, attributes, tokens, refs, write_count
|
|
89
89
|
FROM known_entries
|
|
90
90
|
WHERE run_id = :parent_run_id;
|
|
91
91
|
|
package/src/hooks/Hooks.js
CHANGED
|
@@ -56,6 +56,7 @@ export default function createHooks(debug = false) {
|
|
|
56
56
|
turn: {
|
|
57
57
|
started: createEvent("turn.started"),
|
|
58
58
|
response: createEvent("turn.response"),
|
|
59
|
+
proposal: createEvent("turn.proposal"),
|
|
59
60
|
proposing: createEvent("turn.proposing"),
|
|
60
61
|
completed: createEvent("turn.completed"),
|
|
61
62
|
},
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
// Tool display order: gather → reason → act → communicate.
|
|
2
2
|
// Position in the list implies priority to the model.
|
|
3
3
|
const TOOL_ORDER = [
|
|
4
|
+
"think",
|
|
5
|
+
"unknown",
|
|
6
|
+
"known",
|
|
4
7
|
"get",
|
|
5
8
|
"set",
|
|
6
|
-
"known",
|
|
7
|
-
"unknown",
|
|
8
9
|
"env",
|
|
9
10
|
"sh",
|
|
10
11
|
"rm",
|
|
11
12
|
"cp",
|
|
12
13
|
"mv",
|
|
13
|
-
"search",
|
|
14
|
-
"summarize",
|
|
15
|
-
"update",
|
|
16
14
|
"ask_user",
|
|
15
|
+
"update",
|
|
16
|
+
"summarize",
|
|
17
|
+
"search",
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
function sortByPriority(names) {
|
|
@@ -2,27 +2,22 @@
|
|
|
2
2
|
// Text goes to the model. Rationale stays in source.
|
|
3
3
|
// Changing ANY line requires reading ALL rationales first.
|
|
4
4
|
const LINES = [
|
|
5
|
-
// --- Syntax: question attr + options in body
|
|
6
5
|
['## <ask_user question="[Question?]">[option1; option2; ...]</ask_user>'],
|
|
7
|
-
|
|
8
|
-
// --- Constraints FIRST: frames correct usage before examples
|
|
9
6
|
[
|
|
10
7
|
"* YOU SHOULD use for decisions, preferences, or approvals the user must make",
|
|
11
|
-
"Positive framing. Shows what ask_user IS for
|
|
8
|
+
"Positive framing. Shows what ask_user IS for.",
|
|
12
9
|
],
|
|
13
10
|
[
|
|
14
11
|
"* YOU SHOULD use <get> to find information before asking the user",
|
|
15
|
-
"Gentle redirect. Encourages self-sufficiency
|
|
12
|
+
"Gentle redirect. Encourages self-sufficiency.",
|
|
16
13
|
],
|
|
17
|
-
|
|
18
|
-
// --- Examples: genuine decision points where user input is valuable
|
|
19
14
|
[
|
|
20
15
|
'Example: <ask_user question="Which test framework?">Mocha; Jest; Node Native</ask_user>',
|
|
21
16
|
"Preference decision. Model truly cannot know this without asking.",
|
|
22
17
|
],
|
|
23
18
|
[
|
|
24
19
|
'Example: <ask_user question="Deploy to staging or production?">staging; production</ask_user>',
|
|
25
|
-
"Consequential action.
|
|
20
|
+
"Consequential action. High-stakes choice.",
|
|
26
21
|
],
|
|
27
22
|
];
|
|
28
23
|
|
|
@@ -2,30 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
Context ceiling enforcement.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Design
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Ceiling = `floor(contextSize × 0.9)`. The 10% headroom is the system's
|
|
8
|
+
operating room for graceful overflow handling. No per-write gating —
|
|
9
|
+
tools run uninterrupted. Enforcement happens at boundaries.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Enforcement Points
|
|
12
12
|
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
1. **Pre-LLM enforce** (`budget.enforce`): checks assembled context
|
|
14
|
+
before the LLM call. If over ceiling → Prompt Demotion (summarize
|
|
15
|
+
the incoming prompt). Model runs in the headroom.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
2. **Post-dispatch Turn Demotion**: after all tools dispatch, check
|
|
18
|
+
context. If over ceiling → demote ALL entries from this turn to
|
|
19
|
+
summary (every scheme except `budget`). Write `budget://` entry
|
|
20
|
+
listing what was demoted. Model sees it next turn and adapts.
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
3. **LLM rejection** (`isContextExceeded`): turn-1 token estimate
|
|
23
|
+
drift causes LLM to reject. Same demotion pattern.
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
4. **AgentLoop recovery**: pre-LLM 413 that Prompt Demotion can't
|
|
26
|
+
resolve. Batch-demote all full entries, budget entry, model gets
|
|
27
|
+
recovery turns. 3 strikes without progress → hard 413 to client.
|
|
28
|
+
Only path where 413 reaches the client.
|
|
23
29
|
|
|
24
|
-
|
|
25
|
-
`finally`. Gates `upsert()`, `promoteByPattern()`, `updateBodyByPattern()`.
|
|
30
|
+
## Files
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
- **budget.js** — Plugin. Pre-LLM enforce hook.
|
|
33
|
+
- **BudgetGuard.js** — `BudgetExceeded` error type, `delta` utility.
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
## Registration
|
|
36
|
+
|
|
37
|
+
- **Hook**: `hooks.budget.enforce` — pre-LLM ceiling check.
|
|
38
|
+
- **Scheme**: `budget://` — logging category, model-visible. `onView`
|
|
39
|
+
renders body at all fidelity levels (summary shows full content).
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { countTokens } from "../../agent/tokens.js";
|
|
2
2
|
|
|
3
|
+
const CEILING_RATIO = Number(process.env.RUMMY_BUDGET_CEILING);
|
|
4
|
+
if (!CEILING_RATIO) throw new Error("RUMMY_BUDGET_CEILING must be set");
|
|
5
|
+
|
|
3
6
|
function measureMessages(messages) {
|
|
4
7
|
return messages.reduce((sum, m) => sum + countTokens(m.content), 0);
|
|
5
8
|
}
|
|
@@ -17,6 +20,7 @@ export default class Budget {
|
|
|
17
20
|
core.hooks.tools.onView("budget", (entry) => entry.body);
|
|
18
21
|
core.hooks.budget = {
|
|
19
22
|
enforce: this.enforce.bind(this),
|
|
23
|
+
postDispatch: this.postDispatch.bind(this),
|
|
20
24
|
};
|
|
21
25
|
}
|
|
22
26
|
|
|
@@ -25,8 +29,6 @@ export default class Budget {
|
|
|
25
29
|
return { messages, rows, demoted: [], assembledTokens: 0, status: 200 };
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
// Prefer actual prompt_tokens from the last API response — the estimate
|
|
29
|
-
// from measureMessages can be wildly off for structured/XML-heavy content.
|
|
30
32
|
const assembledTokens =
|
|
31
33
|
lastPromptTokens > 0 ? lastPromptTokens : measureMessages(messages);
|
|
32
34
|
|
|
@@ -34,7 +36,7 @@ export default class Budget {
|
|
|
34
36
|
`[RUMMY] Budget enforce: ${assembledTokens} tokens (${lastPromptTokens > 0 ? "actual" : "estimated"}), ceiling ${contextSize}, ${rows.length} rows`,
|
|
35
37
|
);
|
|
36
38
|
|
|
37
|
-
const ceiling = Math.floor(contextSize *
|
|
39
|
+
const ceiling = Math.floor(contextSize * CEILING_RATIO);
|
|
38
40
|
if (assembledTokens > ceiling) {
|
|
39
41
|
const overflow = assembledTokens - ceiling;
|
|
40
42
|
console.warn(
|
|
@@ -52,4 +54,59 @@ export default class Budget {
|
|
|
52
54
|
|
|
53
55
|
return { messages, rows, demoted: [], assembledTokens, status: 200 };
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
async postDispatch({
|
|
59
|
+
contextSize,
|
|
60
|
+
messages,
|
|
61
|
+
rows,
|
|
62
|
+
runId,
|
|
63
|
+
loopId,
|
|
64
|
+
turn,
|
|
65
|
+
db,
|
|
66
|
+
store,
|
|
67
|
+
}) {
|
|
68
|
+
if (!contextSize) return null;
|
|
69
|
+
|
|
70
|
+
const postBudget = await this.enforce({
|
|
71
|
+
contextSize,
|
|
72
|
+
messages,
|
|
73
|
+
rows,
|
|
74
|
+
lastPromptTokens: 0,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (postBudget.status !== 413) return null;
|
|
78
|
+
|
|
79
|
+
// Demote this turn's entries
|
|
80
|
+
const demotedEntries = await db.demote_turn_entries.all({
|
|
81
|
+
run_id: runId,
|
|
82
|
+
turn,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Also summarize the prompt
|
|
86
|
+
const promptRow = rows.find((r) => r.scheme === "prompt");
|
|
87
|
+
if (promptRow) {
|
|
88
|
+
await store.setFidelity(runId, promptRow.path, "summary");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write budget entry
|
|
92
|
+
const ceiling = Math.floor(contextSize * CEILING_RATIO);
|
|
93
|
+
const totalDemoted = demotedEntries.reduce((s, r) => s + r.tokens, 0);
|
|
94
|
+
const pathList = demotedEntries
|
|
95
|
+
.map((r) => `${r.path} (${r.tokens} tokens)`)
|
|
96
|
+
.join("\n");
|
|
97
|
+
const body = [
|
|
98
|
+
`Error 413: Context overflowed by ${postBudget.overflow} tokens.`,
|
|
99
|
+
`${demotedEntries.length} entries (${totalDemoted} tokens total) demoted. Budget: ${ceiling} tokens.`,
|
|
100
|
+
pathList,
|
|
101
|
+
].join("\n");
|
|
102
|
+
|
|
103
|
+
await store.upsert(runId, turn, `budget://${loopId}/${turn}`, body, 413, {
|
|
104
|
+
loopId,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
target: ceiling,
|
|
109
|
+
promptPath: promptRow?.path ?? null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
55
112
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure recovery state transition — exported for testing.
|
|
3
|
+
*
|
|
4
|
+
* @param {object|null} recovery Current recovery state.
|
|
5
|
+
* @param {{ assembledTokens: number, budgetRecovery?: { target: number, promptPath: string|null } }} result
|
|
6
|
+
* @returns {{ next: object|null, action: null|'restore'|'hard413', promptPath: string|null }}
|
|
7
|
+
*/
|
|
8
|
+
export function advanceRecovery(recovery, result) {
|
|
9
|
+
// Initialise or update recovery state from a new Turn Demotion event.
|
|
10
|
+
if (result.budgetRecovery) {
|
|
11
|
+
if (!recovery) {
|
|
12
|
+
recovery = {
|
|
13
|
+
target: result.budgetRecovery.target,
|
|
14
|
+
promptPath: result.budgetRecovery.promptPath,
|
|
15
|
+
strikes: 0,
|
|
16
|
+
lastTokens: result.assembledTokens,
|
|
17
|
+
};
|
|
18
|
+
} else {
|
|
19
|
+
// Re-overflow during recovery: tighten target, don't count as strike.
|
|
20
|
+
recovery = {
|
|
21
|
+
...recovery,
|
|
22
|
+
target: Math.min(recovery.target, result.budgetRecovery.target),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (recovery === null) return { next: null, action: null, promptPath: null };
|
|
28
|
+
|
|
29
|
+
const current = result.assembledTokens;
|
|
30
|
+
|
|
31
|
+
if (current <= recovery.target) {
|
|
32
|
+
return { next: null, action: "restore", promptPath: recovery.promptPath };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const noProgress = current >= recovery.lastTokens && !result.budgetRecovery;
|
|
36
|
+
const strikes = noProgress ? recovery.strikes + 1 : 0;
|
|
37
|
+
|
|
38
|
+
if (strikes >= 3) {
|
|
39
|
+
return { next: null, action: "hard413", promptPath: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
next: { ...recovery, strikes, lastTokens: current },
|
|
44
|
+
action: null,
|
|
45
|
+
promptPath: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/plugins/cp/cpDoc.js
CHANGED
|
@@ -2,27 +2,22 @@
|
|
|
2
2
|
// Text goes to the model. Rationale stays in source.
|
|
3
3
|
// Changing ANY line requires reading ALL rationales first.
|
|
4
4
|
const LINES = [
|
|
5
|
-
// --- Syntax: path attr = source, body = destination
|
|
6
5
|
['## <cp path="[source]">[destination]</cp> - Copy a file or entry'],
|
|
7
|
-
|
|
8
|
-
// --- Examples: single copy, glob batch, cross-scheme
|
|
9
6
|
[
|
|
10
7
|
'Example: <cp path="src/config.js">src/config.backup.js</cp>',
|
|
11
8
|
"Simple file copy. Path = source, body = destination.",
|
|
12
9
|
],
|
|
13
10
|
[
|
|
14
11
|
'Example: <cp path="known://plan_*">known://archive_</cp>',
|
|
15
|
-
"Glob batch copy across known entries.
|
|
12
|
+
"Glob batch copy across known entries.",
|
|
16
13
|
],
|
|
17
|
-
|
|
18
|
-
// --- Constraints
|
|
19
14
|
[
|
|
20
15
|
"* Source path accepts patterns: `src/*.js`, `known://draft_*`",
|
|
21
|
-
"Pattern support
|
|
16
|
+
"Pattern support consistent with get/rm.",
|
|
22
17
|
],
|
|
23
18
|
[
|
|
24
|
-
"* Use `preview` to check matches before bulk copy",
|
|
25
|
-
"Safety pattern consistent with
|
|
19
|
+
"* Use `preview` to check matches before pattern-based bulk copy",
|
|
20
|
+
"Safety pattern consistent with rm.",
|
|
26
21
|
],
|
|
27
22
|
];
|
|
28
23
|
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
// Text goes to the model. Rationale stays in source.
|
|
3
3
|
// Changing ANY line requires reading ALL rationales first.
|
|
4
4
|
const LINES = [
|
|
5
|
-
// --- Syntax
|
|
6
5
|
["## <env>[command]</env> - Run an exploratory shell command"],
|
|
7
|
-
|
|
8
|
-
// --- Examples: version check and git status — safe, read-only commands
|
|
9
6
|
[
|
|
10
7
|
"Example: <env>npm --version</env>",
|
|
11
8
|
"Version check. Safe, no side effects.",
|
|
@@ -14,15 +11,13 @@ const LINES = [
|
|
|
14
11
|
"Example: <env>git log --oneline -5</env>",
|
|
15
12
|
"Git history. Shows env for read-only investigation.",
|
|
16
13
|
],
|
|
17
|
-
|
|
18
|
-
// --- Constraints: hard boundaries
|
|
19
14
|
[
|
|
20
15
|
'* YOU MUST NOT use <env/> to read or list files — use <get path="*" preview/> instead',
|
|
21
|
-
"Prevents cat/ls through shell. Forces file access through get
|
|
16
|
+
"Prevents cat/ls through shell. Forces file access through get.",
|
|
22
17
|
],
|
|
23
18
|
[
|
|
24
|
-
"* YOU MUST use <
|
|
25
|
-
"Separates exploration from action. env = observe
|
|
19
|
+
"* YOU MUST NOT use <env/> for commands with side effects",
|
|
20
|
+
"Separates exploration from action. env = observe only.",
|
|
26
21
|
],
|
|
27
22
|
];
|
|
28
23
|
|
package/src/plugins/get/get.js
CHANGED
|
@@ -90,9 +90,7 @@ export default class Get {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
const VALID_FIDELITY = {
|
|
93
|
-
stored: 1,
|
|
94
93
|
summary: 1,
|
|
95
|
-
index: 1,
|
|
96
94
|
full: 1,
|
|
97
95
|
archive: 1,
|
|
98
96
|
};
|
|
@@ -118,11 +116,11 @@ export default class Get {
|
|
|
118
116
|
{ loopId },
|
|
119
117
|
);
|
|
120
118
|
} else {
|
|
121
|
-
const total = matches.reduce((s, m) => s + m.
|
|
119
|
+
const total = matches.reduce((s, m) => s + m.tokens, 0);
|
|
122
120
|
const paths = matches.map((m) => m.path).join(", ");
|
|
123
121
|
const body =
|
|
124
122
|
matches.length > 0
|
|
125
|
-
? `${paths}
|
|
123
|
+
? `${paths} promoted to full (${total} tokens)`
|
|
126
124
|
: `${target} not found`;
|
|
127
125
|
await store.upsert(runId, turn, entry.resultPath, body, 200, {
|
|
128
126
|
loopId,
|
|
@@ -2,49 +2,42 @@
|
|
|
2
2
|
// Text goes to the model. Rationale stays in source.
|
|
3
3
|
// Changing ANY line requires reading ALL rationales first.
|
|
4
4
|
const LINES = [
|
|
5
|
-
// --- Syntax: body-form is the primary invocation (simplest)
|
|
6
5
|
["## <get>[path/to/file]</get> - Load a file or entry into context"],
|
|
7
|
-
|
|
8
|
-
// --- Examples: 3 examples covering single file, known recall, and content search
|
|
9
6
|
[
|
|
10
7
|
"Example: <get>src/app.js</get>",
|
|
11
|
-
"Simplest form. Body = path.
|
|
8
|
+
"Simplest form. Body = path.",
|
|
12
9
|
],
|
|
13
10
|
[
|
|
14
11
|
'Example: <get path="known://*">auth</get>',
|
|
15
|
-
"Keyword recall: glob in path, search term in body.
|
|
12
|
+
"Keyword recall: glob in path, search term in body.",
|
|
16
13
|
],
|
|
17
14
|
[
|
|
18
15
|
'Example: <get path="src/**/*.js" preview>authentication</get>',
|
|
19
|
-
"Full pattern: recursive glob + preview + content filter.
|
|
16
|
+
"Full pattern: recursive glob + preview + content filter.",
|
|
20
17
|
],
|
|
21
|
-
|
|
22
|
-
// --- Partial read: line/limit — show before constraints so model sees it as a first-class pattern
|
|
23
18
|
[
|
|
24
19
|
'Example: <get path="src/agent/AgentLoop.js" line="644" limit="80"/>',
|
|
25
|
-
"Partial read. Returns lines 644–723
|
|
20
|
+
"Partial read. Returns lines 644–723 without promoting.",
|
|
26
21
|
],
|
|
27
|
-
|
|
28
|
-
// --- Constraints: RFC-style. Each prevents a specific failure mode.
|
|
29
22
|
[
|
|
30
23
|
"* Paths accept patterns: `src/**/*.js`, `known://api_*`",
|
|
31
|
-
"Reinforces picomatch patterns work everywhere
|
|
24
|
+
"Reinforces picomatch patterns work everywhere.",
|
|
32
25
|
],
|
|
33
26
|
[
|
|
34
|
-
"* `preview`
|
|
35
|
-
"Budget-awareness.
|
|
27
|
+
"* `preview` lists matches without loading into context",
|
|
28
|
+
"Budget-awareness. Preview avoids promotion.",
|
|
36
29
|
],
|
|
37
30
|
[
|
|
38
31
|
"* Body text filters results by content match",
|
|
39
|
-
"
|
|
32
|
+
"Body = filter, not just path.",
|
|
40
33
|
],
|
|
41
34
|
[
|
|
42
35
|
"* `line` and `limit` read a slice without promoting — patterns not allowed",
|
|
43
|
-
"
|
|
36
|
+
"Partial read is safe: context budget unaffected.",
|
|
44
37
|
],
|
|
45
38
|
[
|
|
46
|
-
'* Use <set path="
|
|
47
|
-
"
|
|
39
|
+
'* Use <set path="src/file.txt" fidelity="summary"/> when the content is irrelevant to save tokens.',
|
|
40
|
+
"Cross-tool lifecycle: get promotes, set demotes.",
|
|
48
41
|
],
|
|
49
42
|
];
|
|
50
43
|
|
package/src/plugins/helpers.js
CHANGED
|
@@ -14,8 +14,8 @@ export async function storePatternResult(
|
|
|
14
14
|
) {
|
|
15
15
|
const slug = await store.slugPath(runId, scheme, path);
|
|
16
16
|
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
17
|
-
const total = matches.reduce((s, m) => s + m.
|
|
18
|
-
const listing = matches.map((m) => `${m.path} (${m.
|
|
17
|
+
const total = matches.reduce((s, m) => s + m.tokens, 0);
|
|
18
|
+
const listing = matches.map((m) => `${m.path} (${m.tokens})`).join("\n");
|
|
19
19
|
const prefix = preview ? "PREVIEW " : "";
|
|
20
20
|
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
21
21
|
await store.upsert(runId, turn, slug, body, 200, { loopId });
|
|
@@ -37,7 +37,6 @@ export default class Instructions {
|
|
|
37
37
|
activeTools.has(n),
|
|
38
38
|
);
|
|
39
39
|
const tools = sorted.join(", ");
|
|
40
|
-
let prompt = preamble.replace("[%TOOLS%]", tools);
|
|
41
40
|
const toolDocs = await this.#core.hooks.instructions.toolDocs.filter(
|
|
42
41
|
{},
|
|
43
42
|
{ toolSet: activeTools },
|
|
@@ -46,7 +45,9 @@ export default class Instructions {
|
|
|
46
45
|
.filter((key) => toolDocs[key])
|
|
47
46
|
.map((key) => toolDocs[key])
|
|
48
47
|
.join("\n\n");
|
|
49
|
-
|
|
48
|
+
let prompt = preamble
|
|
49
|
+
.replace("[%TOOLS%]", tools)
|
|
50
|
+
.replace("[%TOOLDOCS%]", docsText);
|
|
50
51
|
if (attrs.persona) prompt += `\n\n## Persona\n\n${attrs.persona}`;
|
|
51
52
|
return prompt;
|
|
52
53
|
}
|