@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.
Files changed (46) hide show
  1. package/.env.example +11 -0
  2. package/README.md +5 -1
  3. package/SPEC.md +31 -17
  4. package/migrations/001_initial_schema.sql +2 -3
  5. package/package.json +1 -1
  6. package/src/agent/AgentLoop.js +50 -151
  7. package/src/agent/KnownStore.js +15 -7
  8. package/src/agent/TurnExecutor.js +75 -318
  9. package/src/agent/XmlParser.js +25 -4
  10. package/src/agent/known_queries.sql +1 -1
  11. package/src/agent/known_store.sql +11 -61
  12. package/src/agent/runs.sql +2 -2
  13. package/src/hooks/Hooks.js +1 -0
  14. package/src/hooks/ToolRegistry.js +6 -5
  15. package/src/plugins/ask_user/ask_userDoc.js +3 -8
  16. package/src/plugins/budget/README.md +26 -18
  17. package/src/plugins/budget/budget.js +60 -3
  18. package/src/plugins/budget/recovery.js +47 -0
  19. package/src/plugins/cp/cpDoc.js +4 -9
  20. package/src/plugins/env/envDoc.js +3 -8
  21. package/src/plugins/get/get.js +2 -4
  22. package/src/plugins/get/getDoc.js +11 -18
  23. package/src/plugins/helpers.js +2 -2
  24. package/src/plugins/instructions/instructions.js +3 -2
  25. package/src/plugins/instructions/preamble.md +27 -16
  26. package/src/plugins/known/known.js +63 -8
  27. package/src/plugins/known/knownDoc.js +10 -14
  28. package/src/plugins/mv/mvDoc.js +6 -21
  29. package/src/plugins/policy/policy.js +47 -0
  30. package/src/plugins/progress/progress.js +9 -45
  31. package/src/plugins/prompt/prompt.js +10 -1
  32. package/src/plugins/rm/rmDoc.js +5 -10
  33. package/src/plugins/rpc/rpc.js +3 -1
  34. package/src/plugins/set/set.js +82 -85
  35. package/src/plugins/set/setDoc.js +28 -41
  36. package/src/plugins/sh/shDoc.js +2 -7
  37. package/src/plugins/summarize/summarize.js +7 -0
  38. package/src/plugins/summarize/summarizeDoc.js +6 -11
  39. package/src/plugins/think/think.js +12 -0
  40. package/src/plugins/think/thinkDoc.js +18 -0
  41. package/src/plugins/unknown/unknown.js +21 -0
  42. package/src/plugins/unknown/unknownDoc.js +9 -14
  43. package/src/plugins/update/update.js +7 -0
  44. package/src/plugins/update/updateDoc.js +6 -11
  45. package/src/server/ClientConnection.js +11 -1
  46. 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, tokens_full, updated_at
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, tokens_full = :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, tokens_full, attributes
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: demote_previous_loop_logging
205
- -- Demote full logging entries from all other loops to summary.
206
- -- Fires at loop start so <previous> entries are already compact.
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
- AND scheme IN (SELECT name FROM schemes WHERE category = 'data')
239
- RETURNING path;
188
+ RETURNING path, tokens;
189
+
@@ -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, tokens_full, refs, write_count
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, tokens_full, refs, write_count
88
+ , hash, attributes, tokens, refs, write_count
89
89
  FROM known_entries
90
90
  WHERE run_id = :parent_run_id;
91
91
 
@@ -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, not just what it isn't.",
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 without forbidding interaction.",
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. Shows ask_user for high-stakes choices.",
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
- ## Files
5
+ ## Design
6
6
 
7
- - **budget.js** Plugin. Pre-LLM enforce, BudgetGuard activation.
8
- - **BudgetGuard.js** Write-layer gate. Installed on KnownStore during
9
- dispatch. Checks token delta on every upsert, promote, and body update.
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
- ## Registration
11
+ ## Enforcement Points
12
12
 
13
- - **Hook**: `hooks.budget.enforce` pre-LLM ceiling check.
14
- - **Hook**: `hooks.budget.activate(store, contextSize, assembledTokens)` install guard.
15
- - **Hook**: `hooks.budget.deactivate(store)` remove guard.
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
- ## Budget Contract
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
- `contextSize` is the ceiling. `countTokens()` is the measurement.
20
- Over = 413. Under = 200. No margins.
22
+ 3. **LLM rejection** (`isContextExceeded`): turn-1 token estimate
23
+ drift causes LLM to reject. Same demotion pattern.
21
24
 
22
- ## BudgetGuard
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
- Installed on KnownStore by TurnExecutor before dispatch, cleared in
25
- `finally`. Gates `upsert()`, `promoteByPattern()`, `updateBodyByPattern()`.
30
+ ## Files
26
31
 
27
- Exemptions: `status >= 400` (error entries), `model_visible = 0` (audit),
28
- `fidelity = "archive"` (not in context).
32
+ - **budget.js** Plugin. Pre-LLM enforce hook.
33
+ - **BudgetGuard.js** `BudgetExceeded` error type, `delta` utility.
29
34
 
30
- On first violation: `BudgetExceeded` thrown, guard trips, all subsequent
31
- writes fail. TurnExecutor catches per-tool, writes 413 result entry.
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 * 0.9);
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
+ }
@@ -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. Shows pattern operations on cp.",
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. Distributes glob teaching beyond get.",
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 get and rm preview.",
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 for proper tracking.",
16
+ "Prevents cat/ls through shell. Forces file access through get.",
22
17
  ],
23
18
  [
24
- "* YOU MUST use <sh/> for commands with side effects",
25
- "Separates exploration from action. env = observe, sh = mutate.",
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
 
@@ -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.tokens_full, 0);
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} loaded into <knowns> (${total} tokens)`
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. Teaches that get is the default read tool.",
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. Cross-scheme hedberg pattern.",
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. Shows all 3 features at once. Body is a filter keyword, never file content.",
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 as the log item without promoting the entry to full. Use summary fidelity to find line numbers, then target the symbol directly.",
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, not just in examples.",
24
+ "Reinforces picomatch patterns work everywhere.",
32
25
  ],
33
26
  [
34
- "* `preview` shows matches without loading into context",
35
- "Budget-awareness. Without this, models load everything and blow context.",
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
- "Generalizes examples 2-3. Body = filter, not just path.",
32
+ "Body = filter, not just path.",
40
33
  ],
41
34
  [
42
35
  "* `line` and `limit` read a slice without promoting — patterns not allowed",
43
- "The no-promotion constraint is what makes partial read safe: context budget is unaffected.",
36
+ "Partial read is safe: context budget unaffected.",
44
37
  ],
45
38
  [
46
- '* Use <set path="..." fidelity="archive"/> to remove loaded content from context',
47
- "Lifecycle: get→set. Load, read, archive. Prevents context hoarding.",
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
 
@@ -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.tokens_full, 0);
18
- const listing = matches.map((m) => `${m.path} (${m.tokens_full})`).join("\n");
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
- if (docsText) prompt += `\n\n${docsText}`;
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
  }