dsh-memory-recall 0.1.0 → 0.2.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/README.md +14 -9
- package/dsh.plugin.json +2 -2
- package/lib/client.js +1490 -1090
- package/package.json +2 -2
- package/src/index.js +185 -156
- package/src/inject.js +87 -0
- package/src/memory-tool.js +167 -136
- package/src/routes.js +205 -153
- package/src/snapshot.js +71 -63
package/src/routes.js
CHANGED
|
@@ -1,153 +1,205 @@
|
|
|
1
|
-
// Host routes for the browser half, and the per-call record the recall card reads.
|
|
2
|
-
//
|
|
3
|
-
// GET /plugin-data/dsh-memory-recall/state → both stores: entries, usage, limits
|
|
4
|
-
// POST /plugin-data/dsh-memory-recall/entry → an edit made in the Memory panel
|
|
5
|
-
// GET /plugin-data/dsh-memory-recall/recall?callId=… → what one session_search call did
|
|
6
|
-
// GET /plugin-data/dsh-memory-recall/chats?session=… → every chat, grouped by workspace, for the @ menu
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
1
|
+
// Host routes for the browser half, and the per-call record the recall card reads.
|
|
2
|
+
//
|
|
3
|
+
// GET /plugin-data/dsh-memory-recall/state → both stores: entries, usage, limits
|
|
4
|
+
// POST /plugin-data/dsh-memory-recall/entry → an edit made in the Memory panel
|
|
5
|
+
// GET /plugin-data/dsh-memory-recall/recall?callId=… → what one session_search call did
|
|
6
|
+
// GET /plugin-data/dsh-memory-recall/chats?session=… → every chat, grouped by workspace, for the @ menu
|
|
7
|
+
// GET /plugin-data/dsh-memory-recall/briefing?session=… → whether that chat's prompt has memory, and which
|
|
8
|
+
//
|
|
9
|
+
// Edits from the panel are the user's own words, so they skip the injection
|
|
10
|
+
// scan the model's writes go through — but not the budget: an over-full store
|
|
11
|
+
// would be cut from nothing, silently, in the next prompt.
|
|
12
|
+
|
|
13
|
+
export const BASE = '/plugin-data/dsh-memory-recall';
|
|
14
|
+
export const STATE_PATH = `${BASE}/state`;
|
|
15
|
+
export const ENTRY_PATH = `${BASE}/entry`;
|
|
16
|
+
export const RECALL_PATH = `${BASE}/recall`;
|
|
17
|
+
export const CHATS_PATH = `${BASE}/chats`;
|
|
18
|
+
export const BRIEFING_PATH = `${BASE}/briefing`;
|
|
19
|
+
|
|
20
|
+
/** A short fingerprint of both stores, to tell whether memory changed since a chat was briefed. */
|
|
21
|
+
export function digestOf(stores = {}) {
|
|
22
|
+
const text = JSON.stringify([stores.user ?? [], stores.memory ?? []]);
|
|
23
|
+
let h = 2166136261;
|
|
24
|
+
for (let i = 0; i < text.length; i += 1) h = Math.imul(h ^ text.charCodeAt(i), 16777619) >>> 0;
|
|
25
|
+
return h.toString(36);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What each chat's prompt was given, by session id: when, why (a new chat,
|
|
30
|
+
* or one reopened after a restart), how many entries, and a fingerprint.
|
|
31
|
+
* Held in memory only — after a dsh restart a chat is briefed again when it
|
|
32
|
+
* is reopened, and recorded again then. Bounded, oldest out first.
|
|
33
|
+
*/
|
|
34
|
+
export function createBriefings(cap = 500, now = () => Date.now()) {
|
|
35
|
+
const byId = new Map();
|
|
36
|
+
return {
|
|
37
|
+
record(sessionId, stores, source) {
|
|
38
|
+
byId.delete(sessionId);
|
|
39
|
+
byId.set(sessionId, {
|
|
40
|
+
at: now(),
|
|
41
|
+
source: ['start', 'compaction', 'update', 'resume'].includes(source) ? source : 'start',
|
|
42
|
+
user: (stores.user ?? []).length,
|
|
43
|
+
memory: (stores.memory ?? []).length,
|
|
44
|
+
digest: digestOf(stores),
|
|
45
|
+
});
|
|
46
|
+
while (byId.size > cap) byId.delete(byId.keys().next().value);
|
|
47
|
+
},
|
|
48
|
+
get: (sessionId) => byId.get(sessionId) ?? null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The chip's view of one chat: briefed or not, and whether memory moved on since. */
|
|
53
|
+
export async function briefingOf(briefings, store, sessionId) {
|
|
54
|
+
const current = await store.load();
|
|
55
|
+
const now = { user: (current.user ?? []).length, memory: (current.memory ?? []).length };
|
|
56
|
+
const rec = sessionId ? briefings?.get(sessionId) : null;
|
|
57
|
+
if (!rec) return { briefed: false, now };
|
|
58
|
+
return { briefed: true, ...rec, changed: digestOf(current) !== rec.digest, now };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What the recall card needs from a result — titles and snippets, not whole chats. */
|
|
62
|
+
export function summarize(result) {
|
|
63
|
+
if (!result) return null;
|
|
64
|
+
const brief = (s) => ({ sessionId: s.sessionId, title: s.title, createdAt: s.createdAt, cwd: s.cwd });
|
|
65
|
+
switch (result.mode) {
|
|
66
|
+
case 'discover':
|
|
67
|
+
return {
|
|
68
|
+
mode: 'discover', engine: result.engine, query: result.query, widened: result.widened,
|
|
69
|
+
results: result.results.map((r) => ({ ...brief(r), snippet: r.match.snippet, matchedWords: r.matchedWords })),
|
|
70
|
+
};
|
|
71
|
+
case 'scroll': return { mode: 'scroll', session: brief(result.session), count: result.messages.length };
|
|
72
|
+
case 'read': return { mode: 'read', session: brief(result.session), count: result.total };
|
|
73
|
+
case 'browse': return { mode: 'browse', sessions: result.sessions.map(brief) };
|
|
74
|
+
default: return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A bounded map of recent session_search calls, by call id. */
|
|
79
|
+
export function createRecallLog(cap = 100, now = () => Date.now()) {
|
|
80
|
+
const calls = new Map();
|
|
81
|
+
return {
|
|
82
|
+
start(callId, info) {
|
|
83
|
+
if (!callId) return;
|
|
84
|
+
calls.set(callId, { ...info, status: 'starting', startedAt: now() });
|
|
85
|
+
while (calls.size > cap) calls.delete(calls.keys().next().value);
|
|
86
|
+
},
|
|
87
|
+
update(callId, patch) {
|
|
88
|
+
const entry = callId ? calls.get(callId) : undefined;
|
|
89
|
+
if (!entry) return;
|
|
90
|
+
const { result, ...rest } = patch;
|
|
91
|
+
Object.assign(entry, rest, result ? { summary: summarize(result) } : {}, { updatedAt: now() });
|
|
92
|
+
},
|
|
93
|
+
get: (callId) => calls.get(callId) ?? null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sendJson(res, status, payload) {
|
|
98
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
99
|
+
res.end(JSON.stringify(payload));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function readBody(req) {
|
|
103
|
+
let text = '';
|
|
104
|
+
for await (const chunk of req ?? []) text += chunk;
|
|
105
|
+
if (!text) return {};
|
|
106
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Both stores as the panel shows them. Re-read from disk, so a hand edit shows at once. */
|
|
110
|
+
export async function stateOf(store, tooling = null) {
|
|
111
|
+
await store.load();
|
|
112
|
+
const one = (target) => ({ entries: store.entries[target], usage: store.usage(target) });
|
|
113
|
+
return { dir: store.dir, user: one('user'), memory: one('memory'), ...(tooling ? { tools: { ...tooling } } : {}) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The @ menu's view of the chat index: the asking chat left out (mentioning
|
|
118
|
+
* the chat you are in is pointless), and its workspace named so the menu can
|
|
119
|
+
* list that one first.
|
|
120
|
+
*/
|
|
121
|
+
export async function chatsFor(chatIndex, sessionId, cwdOf = async () => null) {
|
|
122
|
+
const index = await chatIndex.list();
|
|
123
|
+
const without = (chats) => chats.filter((c) => c.sessionId !== sessionId);
|
|
124
|
+
let current = sessionId ? chatIndex.workspaceOf(index, sessionId, null) : null;
|
|
125
|
+
if (!current && sessionId) current = chatIndex.workspaceOf(index, sessionId, await cwdOf(sessionId));
|
|
126
|
+
return {
|
|
127
|
+
currentWorkspaceId: current?.id ?? null,
|
|
128
|
+
workspaces: index.workspaces
|
|
129
|
+
.map((w) => ({ id: w.id, title: w.title, path: w.path, chats: without(w.chats) }))
|
|
130
|
+
.filter((w) => w.chats.length),
|
|
131
|
+
ungrouped: without(index.ungrouped),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {object} webServer ctx.webServer
|
|
137
|
+
* @param {object} p {store, log, chatIndex?, cwdOf?}
|
|
138
|
+
* @returns {() => void} disposer
|
|
139
|
+
*/
|
|
140
|
+
export function registerRoutes(webServer, { store, log, chatIndex, cwdOf, tooling, briefings = null }) {
|
|
141
|
+
const off = [];
|
|
142
|
+
off.push(webServer.register({
|
|
143
|
+
kind: 'exact',
|
|
144
|
+
path: STATE_PATH,
|
|
145
|
+
handler: async (req = {}, res) => {
|
|
146
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') return sendJson(res, 405, { error: 'method not allowed' });
|
|
147
|
+
try {
|
|
148
|
+
sendJson(res, 200, await stateOf(store, tooling));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
sendJson(res, 500, { error: `could not read memory: ${error?.message ?? error}` });
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
off.push(webServer.register({
|
|
155
|
+
kind: 'exact',
|
|
156
|
+
path: ENTRY_PATH,
|
|
157
|
+
handler: async (req = {}, res) => {
|
|
158
|
+
if ((req.method || 'GET').toUpperCase() !== 'POST') return sendJson(res, 405, { error: 'method not allowed' });
|
|
159
|
+
const body = await readBody(req);
|
|
160
|
+
if (!body || typeof body !== 'object') return sendJson(res, 400, { error: 'the body must be a JSON object' });
|
|
161
|
+
const { target, action, content, old_text: oldText } = body;
|
|
162
|
+
let result;
|
|
163
|
+
if (action === 'add') result = await store.add(target, content, { scan: false });
|
|
164
|
+
else if (action === 'replace') result = await store.replace(target, oldText, content, { scan: false });
|
|
165
|
+
else if (action === 'remove') result = await store.remove(target, oldText);
|
|
166
|
+
else return sendJson(res, 400, { error: "action must be 'add', 'replace' or 'remove'" });
|
|
167
|
+
sendJson(res, result.success ? 200 : 400, { result, state: await stateOf(store, tooling) });
|
|
168
|
+
},
|
|
169
|
+
}));
|
|
170
|
+
off.push(webServer.register({
|
|
171
|
+
kind: 'exact',
|
|
172
|
+
path: RECALL_PATH,
|
|
173
|
+
handler: async (req = {}, res) => {
|
|
174
|
+
const callId = new URL(req.url ?? '/', 'http://x').searchParams.get('callId');
|
|
175
|
+
if (!callId) return sendJson(res, 400, { error: 'missing callId' });
|
|
176
|
+
const entry = log.get(callId);
|
|
177
|
+
sendJson(res, entry ? 200 : 404, entry ?? { error: 'no record of that call (dsh may have restarted since)' });
|
|
178
|
+
},
|
|
179
|
+
}));
|
|
180
|
+
off.push(webServer.register({
|
|
181
|
+
kind: 'exact',
|
|
182
|
+
path: BRIEFING_PATH,
|
|
183
|
+
handler: async (req = {}, res) => {
|
|
184
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') return sendJson(res, 405, { error: 'method not allowed' });
|
|
185
|
+
const sessionId = new URL(req.url ?? '/', 'http://x').searchParams.get('session');
|
|
186
|
+
sendJson(res, 200, await briefingOf(briefings, store, sessionId));
|
|
187
|
+
},
|
|
188
|
+
}));
|
|
189
|
+
if (chatIndex) {
|
|
190
|
+
off.push(webServer.register({
|
|
191
|
+
kind: 'exact',
|
|
192
|
+
path: CHATS_PATH,
|
|
193
|
+
handler: async (req = {}, res) => {
|
|
194
|
+
if ((req.method || 'GET').toUpperCase() !== 'GET') return sendJson(res, 405, { error: 'method not allowed' });
|
|
195
|
+
const sessionId = new URL(req.url ?? '/', 'http://x').searchParams.get('session');
|
|
196
|
+
try {
|
|
197
|
+
sendJson(res, 200, await chatsFor(chatIndex, sessionId, cwdOf));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
sendJson(res, 503, { error: String(error?.message ?? error) });
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
return () => { for (const dispose of off) dispose(); };
|
|
205
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -1,63 +1,71 @@
|
|
|
1
|
-
// What a new chat's system prompt says about memory.
|
|
2
|
-
//
|
|
3
|
-
// Rendered ONCE, when the chat's agent starts, and never again for that chat.
|
|
4
|
-
// dsh keeps the prompt prefix cacheable only while its text is unchanged —
|
|
5
|
-
// "any change may invalidate reuse from the first changed token" — so a
|
|
6
|
-
// memory saved mid-chat is written to disk at once but reaches the prompt in
|
|
7
|
-
// the next new chat. The guidance says so, so the model does not expect to
|
|
8
|
-
// see its own save reflected, and does not save twice.
|
|
9
|
-
|
|
10
|
-
import { TARGETS, joinEntries, sizeOf } from './store.js';
|
|
11
|
-
|
|
12
|
-
/** After prompt-notes (950) and every first-party instruction, before the environment suffix (10000). */
|
|
13
|
-
export const SECTION_ORDER = 9500;
|
|
14
|
-
export const SECTION_NAME = 'dsh-memory-recall:memory';
|
|
15
|
-
|
|
16
|
-
const RULE = '═'.repeat(46);
|
|
17
|
-
|
|
18
|
-
export const GUIDANCE = [
|
|
19
|
-
'# Memory',
|
|
20
|
-
'',
|
|
21
|
-
'You have a persistent memory that carries across chats
|
|
22
|
-
'
|
|
23
|
-
'',
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
'-
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
1
|
+
// What a new chat's system prompt says about memory.
|
|
2
|
+
//
|
|
3
|
+
// Rendered ONCE, when the chat's agent starts, and never again for that chat.
|
|
4
|
+
// dsh keeps the prompt prefix cacheable only while its text is unchanged —
|
|
5
|
+
// "any change may invalidate reuse from the first changed token" — so a
|
|
6
|
+
// memory saved mid-chat is written to disk at once but reaches the prompt in
|
|
7
|
+
// the next new chat. The guidance says so, so the model does not expect to
|
|
8
|
+
// see its own save reflected, and does not save twice.
|
|
9
|
+
|
|
10
|
+
import { TARGETS, joinEntries, sizeOf } from './store.js';
|
|
11
|
+
|
|
12
|
+
/** After prompt-notes (950) and every first-party instruction, before the environment suffix (10000). */
|
|
13
|
+
export const SECTION_ORDER = 9500;
|
|
14
|
+
export const SECTION_NAME = 'dsh-memory-recall:memory';
|
|
15
|
+
|
|
16
|
+
const RULE = '═'.repeat(46);
|
|
17
|
+
|
|
18
|
+
export const GUIDANCE = [
|
|
19
|
+
'# Memory',
|
|
20
|
+
'',
|
|
21
|
+
'You have a persistent memory that carries across chats. It reaches you as a "memory" context message in this conversation:',
|
|
22
|
+
'at the start of a chat, again whenever it changes, and again after the conversation is compacted. The latest one is current.',
|
|
23
|
+
'',
|
|
24
|
+
'**Use it without being asked.** Its entries are things you already know about the user and their setup, learned in earlier',
|
|
25
|
+
"chats. From your first reply on: follow the user's preferences, rely on the environment facts and conventions instead of",
|
|
26
|
+
'rediscovering them or asking the user again, and apply the lessons before repeating a past mistake. Do not announce that you',
|
|
27
|
+
'checked memory — just act on it. If an entry turns out to be wrong or outdated, correct it with the `memory` tool.',
|
|
28
|
+
'',
|
|
29
|
+
"- Save with the `memory` tool. Target 'user' is who the user is: name, role, preferences, how they like to work.",
|
|
30
|
+
" Target 'memory' is your own notes: environment facts, conventions, tool quirks, lessons that apply to every task.",
|
|
31
|
+
'- Save durable facts, one short fact per entry, when you learn them — not task progress, one-off details, or anything easy to look up again.',
|
|
32
|
+
' A save shows up in an updated memory message on your next step; do not save it twice.',
|
|
33
|
+
'- The user sees each save as a "Saved to memory" note in the chat and can edit or delete entries in the Memory panel.',
|
|
34
|
+
' Never save secrets or credentials, or anything the user would not want written down.',
|
|
35
|
+
'- When the user refers to an earlier conversation ("what did we decide about…", "like last time"), use `session_search`.',
|
|
36
|
+
' It asks the user for permission to read past chats first. If they decline, say so and carry on without it; do not ask again in the same reply.',
|
|
37
|
+
'- A user message carrying [workspace "Name" — folder] means the user picked that workspace with @: pass it as `workspace` to',
|
|
38
|
+
' session_search. A chat the user @-mentions arrives as a "Referenced sessions" snapshot — use that directly, no search needed.',
|
|
39
|
+
].join('\n');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The note when the tools failed to load: the memory below can be read, but
|
|
43
|
+
* nothing can be saved or searched — and the model must say so rather than
|
|
44
|
+
* improvise a substitute (it once wrote a notes file instead).
|
|
45
|
+
*/
|
|
46
|
+
export const GUIDANCE_NO_TOOLS = [
|
|
47
|
+
'# Memory',
|
|
48
|
+
'',
|
|
49
|
+
'You have a persistent memory that carries across chats; it reaches you as a "memory" context message in this conversation.',
|
|
50
|
+
'Its tools (`memory` and `session_search`) failed to load in this dsh, so right now you cannot save to memory or search',
|
|
51
|
+
'earlier chats. If the user asks you to remember something, tell them memory saving is unavailable in this session and',
|
|
52
|
+
'that they can add it themselves in the Memory panel in the sidebar. Do not substitute a file or anything else for it.',
|
|
53
|
+
].join('\n');
|
|
54
|
+
|
|
55
|
+
/** One store as a prompt block, or '' when it is empty. */
|
|
56
|
+
export function renderBlock(target, entries, limit) {
|
|
57
|
+
if (!entries.length) return '';
|
|
58
|
+
const chars = sizeOf(entries);
|
|
59
|
+
const percent = limit > 0 ? Math.min(100, Math.floor((chars / limit) * 100)) : 0;
|
|
60
|
+
const usage = `${percent}% — ${chars.toLocaleString('en-US')}/${limit.toLocaleString('en-US')} chars`;
|
|
61
|
+
return `${RULE}\n${TARGETS[target].title} [${usage}]\n${RULE}\n${joinEntries(entries)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The system-prompt section: how memory works, and nothing that changes. The
|
|
66
|
+
* entries themselves arrive as a context message (src/inject.js), so this
|
|
67
|
+
* text stays identical for the whole chat and the prompt prefix stays cached.
|
|
68
|
+
*/
|
|
69
|
+
export function renderSection(_stores, _limits, { tools = true } = {}) {
|
|
70
|
+
return tools ? GUIDANCE : GUIDANCE_NO_TOOLS;
|
|
71
|
+
}
|