helpers.mjs
148 lines 5.3 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Shared helpers for MCP prompts (Issue #1 Phase B + F5 prefill).
3 */
4
5 import { readNote } from '../../lib/vault.mjs';
6 import { noteToMarkdown } from '../resources/note.mjs';
7 import { trySampling } from '../sampling.mjs';
8
9 export const MAX_EMBEDDED_NOTES = 12;
10 export const MAX_ENTITY_NOTES = 20;
11 export const PROJECT_SUMMARY_NOTES = 15;
12 export const CONTENT_PLAN_NOTES = 25;
13
14 /** @param {string} text */
15 export function textContent(text) {
16 return { type: 'text', text };
17 }
18
19 /**
20 * @param {string} uri
21 * @param {string} text markdown body
22 */
23 export function embeddedMarkdownResource(uri, text) {
24 return {
25 type: 'resource',
26 resource: {
27 uri,
28 mimeType: 'text/markdown',
29 text,
30 },
31 };
32 }
33
34 /**
35 * @param {import('../../lib/config.mjs').loadConfig extends () => infer R ? R : never} config
36 * @param {string} relPath vault-relative
37 */
38 export function embeddedNoteFromPath(config, relPath) {
39 const norm = relPath.replace(/\\/g, '/').replace(/^\//, '');
40 const note = readNote(config.vault_path, norm);
41 const uri = `knowtation://vault/${norm}`;
42 return embeddedMarkdownResource(uri, noteToMarkdown(note));
43 }
44
45 /** @param {string} [body] @param {number} [max] */
46 export function snippet(body, max = 200) {
47 const t = (body || '').replace(/\s+/g, ' ').trim();
48 if (t.length <= max) return t;
49 return `${t.slice(0, max)}…`;
50 }
51
52 /** @param {string | undefined} s @param {number} def */
53 export function parseIntSafe(s, def) {
54 const n = parseInt(String(s ?? '').trim(), 10);
55 return Number.isFinite(n) ? n : def;
56 }
57
58 export const MAX_MEMORY_EVENTS = 30;
59
60 /**
61 * Format memory events as a markdown text block for prompt embedding.
62 * @param {object} config — loadConfig() result
63 * @param {{ type?: string, limit?: number, since?: string, until?: string }} [opts]
64 * @returns {{ text: string, count: number }}
65 */
66 export function formatMemoryEvents(config, opts = {}) {
67 try {
68 const { createMemoryManager } = require('../../lib/memory.mjs');
69 const mm = createMemoryManager(config);
70 const events = mm.list({
71 type: opts.type || undefined,
72 limit: Math.min(opts.limit ?? 20, MAX_MEMORY_EVENTS),
73 since: opts.since || undefined,
74 until: opts.until || undefined,
75 });
76 if (events.length === 0) return { text: '(No memory events found.)', count: 0 };
77 const lines = events.map((e) => {
78 const summary = JSON.stringify(e.data).slice(0, 200);
79 return `- **${e.ts}** [${e.type}] ${summary}`;
80 });
81 return { text: lines.join('\n'), count: events.length };
82 } catch (_) {
83 return { text: '(Memory not available.)', count: 0 };
84 }
85 }
86
87 /**
88 * Async version for use in prompt handlers (dynamic import avoids CJS/ESM issues).
89 * @param {object} config
90 * @param {{ type?: string, limit?: number, since?: string, until?: string }} [opts]
91 * @returns {Promise<{ text: string, count: number }>}
92 */
93 export async function formatMemoryEventsAsync(config, opts = {}) {
94 try {
95 const { createMemoryManager } = await import('../../lib/memory.mjs');
96 const mm = createMemoryManager(config);
97 const events = mm.list({
98 type: opts.type || undefined,
99 limit: Math.min(opts.limit ?? 20, MAX_MEMORY_EVENTS),
100 since: opts.since || undefined,
101 until: opts.until || undefined,
102 });
103 if (events.length === 0) return { text: '(No memory events found.)', count: 0 };
104 const lines = events.map((e) => {
105 const summary = JSON.stringify(e.data).slice(0, 200);
106 return `- **${e.ts}** [${e.type}] ${summary}`;
107 });
108 return { text: lines.join('\n'), count: events.length };
109 } catch (_) {
110 return { text: '(Memory not available.)', count: 0 };
111 }
112 }
113
114 /**
115 * Phase F5 — attempt to prefill the assistant turn via sampling.
116 * Extracts the last user-role text from the messages array and asks the client
117 * LLM for a draft response. Returns the original result with an appended assistant
118 * message when sampling succeeds; otherwise returns it unchanged.
119 *
120 * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} mcpServer
121 * @param {{ description?: string, messages: Array<{ role: string, content: unknown }> }} promptResult
122 * @returns {Promise<{ description?: string, messages: Array<{ role: string, content: unknown }> }>}
123 */
124 export async function maybeAppendSamplingPrefill(mcpServer, promptResult) {
125 if (!promptResult || !Array.isArray(promptResult.messages) || promptResult.messages.length === 0) {
126 return promptResult;
127 }
128 const lastAssistant = promptResult.messages[promptResult.messages.length - 1];
129 if (lastAssistant?.role === 'assistant') return promptResult;
130
131 const userMessages = promptResult.messages.filter((m) => m.role === 'user');
132 if (userMessages.length === 0) return promptResult;
133
134 const last = userMessages[userMessages.length - 1];
135 const userText = typeof last.content === 'string'
136 ? last.content
137 : last.content?.type === 'text' ? last.content.text : null;
138 if (!userText) return promptResult;
139
140 const system = 'You are a helpful knowledge assistant. Provide a thorough but concise draft response to the following prompt. The user will refine your draft.';
141 const draft = await trySampling(mcpServer, { system, user: userText.slice(0, 16000), maxTokens: 1024 });
142 if (!draft) return promptResult;
143
144 return {
145 ...promptResult,
146 messages: [...promptResult.messages, { role: 'assistant', content: textContent(draft) }],
147 };
148 }
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago