From 6028fda2db86a8d238c207841692e6d8c46eed86 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:27:07 +0530 Subject: [PATCH] moved to a full agent --- apps/x/packages/core/src/agents/runtime.ts | 52 +- .../src/application/assistant/instructions.ts | 17 +- .../core/src/application/lib/builtin-tools.ts | 9 +- .../core/src/knowledge/agent_notes.ts | 493 ++++-------------- .../core/src/knowledge/agent_notes_agent.ts | 53 ++ 5 files changed, 218 insertions(+), 406 deletions(-) create mode 100644 apps/x/packages/core/src/knowledge/agent_notes_agent.ts diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index b14339ef..3fc28321 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -30,6 +30,7 @@ import { getRaw as getNoteCreationRaw } from "../knowledge/note_creation.js"; import { getRaw as getLabelingAgentRaw } from "../knowledge/labeling_agent.js"; import { getRaw as getNoteTaggingAgentRaw } from "../knowledge/note_tagging_agent.js"; import { getRaw as getInlineTaskAgentRaw } from "../knowledge/inline_task_agent.js"; +import { getRaw as getAgentNotesAgentRaw } from "../knowledge/agent_notes_agent.js"; const AGENT_NOTES_DIR = path.join(WorkDir, 'knowledge', 'agent-notes'); @@ -57,6 +58,30 @@ function loadAgentNotesContext(): string | null { } } catch { /* ignore */ } + // List other agent-notes files for on-demand access + const otherFiles: string[] = []; + const skipFiles = new Set(['user.md', 'preferences.md', 'inbox.md']); + try { + if (fs.existsSync(AGENT_NOTES_DIR)) { + function listMdFiles(dir: string, prefix: string) { + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + listMdFiles(fullPath, `${prefix}${entry}/`); + } else if (entry.endsWith('.md') && !skipFiles.has(`${prefix}${entry}`)) { + otherFiles.push(`${prefix}${entry}`); + } + } + } + listMdFiles(AGENT_NOTES_DIR, ''); + } + } catch { /* ignore */ } + + if (otherFiles.length > 0) { + sections.push(`## More Specific Preferences\nFor more specific preferences, you can read these files using workspace-readFile. Only read them when relevant to the current task.\n\n${otherFiles.map(f => `- knowledge/agent-notes/${f}`).join('\n')}`); + } + if (sections.length === 0) return null; return `# Agent Memory\n\n${sections.join('\n\n')}`; } @@ -448,6 +473,31 @@ export async function loadAgent(id: string): Promise> { return agent; } + if (id === 'agent_notes_agent') { + const agentNotesAgentRaw = getAgentNotesAgentRaw(); + let agent: z.infer = { + name: id, + instructions: agentNotesAgentRaw, + }; + + if (agentNotesAgentRaw.startsWith("---")) { + const end = agentNotesAgentRaw.indexOf("\n---", 3); + if (end !== -1) { + const fm = agentNotesAgentRaw.slice(3, end).trim(); + const content = agentNotesAgentRaw.slice(end + 4).trim(); + const yaml = parse(fm); + const parsed = Agent.omit({ name: true, instructions: true }).parse(yaml); + agent = { + ...agent, + ...parsed, + instructions: content, + }; + } + } + + return agent; + } + const repo = container.resolve('agentsRepo'); return await repo.fetch(id); } @@ -803,7 +853,7 @@ export async function* streamAgent({ const provider = await isSignedIn() ? await getGatewayProvider() : createProvider(modelConfig.provider); - const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent"]; + const knowledgeGraphAgents = ["note_creation", "email-draft", "meeting-prep", "labeling_agent", "note_tagging_agent", "agent_notes_agent"]; const modelId = (knowledgeGraphAgents.includes(state.agentName!) && modelConfig.knowledgeGraphModel) ? modelConfig.knowledgeGraphModel : modelConfig.model; diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index a6eff725..d39a0d63 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -42,11 +42,12 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects, Use the \`save-to-memory\` tool to note things worth remembering about the user. This builds a persistent profile that helps you serve them better over time. Call it proactively — don't ask permission. **When to save:** -- User states a preference: "I prefer bullet points" → save as preference -- User corrects your style: "too formal, keep it casual" → save as style -- You learn about their relationships: "Monica is my co-founder" → save as people -- You notice workflow patterns: "no meetings before 11am" → save as routine -- User gives explicit instructions: "never use em-dashes" → save as preference +- User states a preference: "I prefer bullet points" +- User corrects your style: "too formal, keep it casual" +- You learn about their relationships: "Monica is my co-founder" +- You notice workflow patterns: "no meetings before 11am" +- User gives explicit instructions: "never use em-dashes" +- User has preferences for specific tasks: "pitch decks should be minimal, max 12 slides" **Capture context, not blanket rules:** - BAD: "User prefers casual tone" — this loses important context @@ -60,12 +61,6 @@ Use the \`save-to-memory\` tool to note things worth remembering about the user. - Things already in the knowledge graph - Information you can derive from reading their notes -**Categories:** -- \`preference\` — rules about how they want things done -- \`style\` — writing and communication patterns (always note the context: who, what type of communication) -- \`people\` — relationship context and per-person tone -- \`routine\` — scheduling, workflow, recurring patterns - ## Memory That Compounds Unlike other AI assistants that start cold every session, you have access to a live knowledge graph that updates itself from Gmail, calendar, and meeting notes (Google Meet, Granola, Fireflies). This isn't just summaries - it's structured extraction of decisions, commitments, open questions, and context, routed to long-lived notes for each person, project, and topic. diff --git a/apps/x/packages/core/src/application/lib/builtin-tools.ts b/apps/x/packages/core/src/application/lib/builtin-tools.ts index 2fd0b4a5..09fed746 100644 --- a/apps/x/packages/core/src/application/lib/builtin-tools.ts +++ b/apps/x/packages/core/src/application/lib/builtin-tools.ts @@ -1261,24 +1261,23 @@ export const BuiltinTools: z.infer = { }, }, 'save-to-memory': { - description: "Save a note about user preferences, style, people, or routines to the agent memory inbox. Use this when you observe something worth remembering about the user — their preferences, communication patterns, relationship context, scheduling habits, or explicit instructions about how they want things done.", + description: "Save a note about the user to the agent memory inbox. Use this when you observe something worth remembering — their preferences, communication patterns, relationship context, scheduling habits, or explicit instructions about how they want things done.", inputSchema: z.object({ note: z.string().describe("The observation or preference to remember. Be specific and concise."), - category: z.enum(['preference', 'style', 'people', 'routine']).describe("Category: 'preference' for rules/preferences, 'style' for writing/communication patterns, 'people' for relationship context, 'routine' for scheduling/workflow patterns"), }), - execute: async ({ note, category }: { note: string; category: string }) => { + execute: async ({ note }: { note: string }) => { const inboxPath = path.join(WorkDir, 'knowledge', 'agent-notes', 'inbox.md'); const dir = path.dirname(inboxPath); await fs.mkdir(dir, { recursive: true }); const timestamp = new Date().toISOString(); - const entry = `\n- [${timestamp}] [${category}] ${note}\n`; + const entry = `\n- [${timestamp}] ${note}\n`; await fs.appendFile(inboxPath, entry, 'utf-8'); return { success: true, - message: `Saved to memory inbox: [${category}] ${note}`, + message: `Saved to memory: ${note}`, }; }, }, diff --git a/apps/x/packages/core/src/knowledge/agent_notes.ts b/apps/x/packages/core/src/knowledge/agent_notes.ts index 2733e776..e8fdb965 100644 --- a/apps/x/packages/core/src/knowledge/agent_notes.ts +++ b/apps/x/packages/core/src/knowledge/agent_notes.ts @@ -1,12 +1,8 @@ import fs from 'fs'; import path from 'path'; -import { generateText } from 'ai'; import { WorkDir } from '../config/config.js'; -import container from '../di/container.js'; -import type { IModelConfigRepo } from '../models/repo.js'; -import { createProvider } from '../models/models.js'; -import { isSignedIn } from '../account/account.js'; -import { getGatewayProvider } from '../models/gateway.js'; +import { createRun, createMessage } from '../runs/runs.js'; +import { bus } from '../runs/bus.js'; import { serviceLogger } from '../services/service_logger.js'; import { loadUserConfig } from '../pre_built/config.js'; import { @@ -23,65 +19,17 @@ const RUNS_BATCH_SIZE = 5; const GMAIL_SYNC_DIR = path.join(WorkDir, 'gmail_sync'); const RUNS_DIR = path.join(WorkDir, 'runs'); const AGENT_NOTES_DIR = path.join(WorkDir, 'knowledge', 'agent-notes'); -const STYLE_DIR = path.join(AGENT_NOTES_DIR, 'style'); const INBOX_FILE = path.join(AGENT_NOTES_DIR, 'inbox.md'); - -const NOTE_FILES = { - preferences: path.join(AGENT_NOTES_DIR, 'preferences.md'), - writingStyle: path.join(STYLE_DIR, 'writing.md'), - emailStyle: path.join(STYLE_DIR, 'email.md'), - slackStyle: path.join(STYLE_DIR, 'slack.md'), - documentsStyle: path.join(STYLE_DIR, 'documents.md'), - people: path.join(AGENT_NOTES_DIR, 'people.md'), - routines: path.join(AGENT_NOTES_DIR, 'routines.md'), - user: path.join(AGENT_NOTES_DIR, 'user.md'), -}; - -const CATEGORY_TO_FILE: Record = { - preference: [NOTE_FILES.preferences], - style: [NOTE_FILES.writingStyle], - people: [NOTE_FILES.people], - routine: [NOTE_FILES.routines], -}; - -// --- LLM helpers --- - -async function getModel() { - const repo = container.resolve('modelConfigRepo'); - const config = await repo.getConfig(); - const provider = await isSignedIn() - ? await getGatewayProvider() - : createProvider(config.provider); - const modelId = config.knowledgeGraphModel || config.model; - return provider.languageModel(modelId); -} - -function stripCodeFences(text: string): string { - return text - .replace(/^```(?:markdown|md)?\s*\n?/, '') - .replace(/\n?```\s*$/, '') - .trim(); -} +const AGENT_ID = 'agent_notes_agent'; // --- File helpers --- function ensureAgentNotesDir(): void { - for (const dir of [AGENT_NOTES_DIR, STYLE_DIR]) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } + if (!fs.existsSync(AGENT_NOTES_DIR)) { + fs.mkdirSync(AGENT_NOTES_DIR, { recursive: true }); } } -function readNoteFile(filePath: string): string { - try { - if (fs.existsSync(filePath)) { - return fs.readFileSync(filePath, 'utf-8'); - } - } catch { /* ignore */ } - return ''; -} - // --- Email scanning --- function findUserSentEmails( @@ -145,35 +93,17 @@ function extractUserPartsFromEmail(content: string, userEmail: string): string | return userSections.length > 0 ? userSections.join('\n\n---\n\n') : null; } -// --- Inbox processing --- +// --- Inbox reading --- -interface InboxEntry { - timestamp: string; - category: string; - note: string; -} - -function readInbox(): InboxEntry[] { - const content = readNoteFile(INBOX_FILE); - if (!content.trim()) { +function readInbox(): string[] { + if (!fs.existsSync(INBOX_FILE)) { return []; } - - const entries: InboxEntry[] = []; - const lines = content.split('\n').filter(l => l.trim()); - - for (const line of lines) { - const match = line.match(/^- \[([^\]]+)\] \[([^\]]+)\] (.+)$/); - if (match) { - entries.push({ - timestamp: match[1], - category: match[2], - note: match[3], - }); - } + const content = fs.readFileSync(INBOX_FILE, 'utf-8').trim(); + if (!content) { + return []; } - - return entries; + return content.split('\n').filter(l => l.trim()); } function clearInbox(): void { @@ -182,112 +112,6 @@ function clearInbox(): void { } } -// --- Note file updates (single LLM call per file) --- - -async function updateNoteFile( - filePath: string, - noteDescription: string, - sourceContent: string, -): Promise { - const model = await getModel(); - const existing = readNoteFile(filePath); - - const system = `You maintain a personal knowledge file about a user. Your job is to update this file by integrating new source material. - -Rules: -- Preserve all existing content that is still relevant -- Add new insights from the source material -- Deduplicate: if an insight is already captured, do not add it again -- Refine existing observations when new evidence supports a more nuanced version -- Keep the file well-organized with clear markdown headings and bullet points -- Be concise — prefer bullet points over paragraphs -- If the file is empty, create initial structure appropriate for: ${noteDescription} -- Output ONLY the complete updated file content, no commentary or explanation`; - - const prompt = `## Current file content: -${existing || '(empty — this is a new file)'} - -## New source material to integrate: -${sourceContent} - -Return the complete updated file:`; - - const result = await generateText({ model, system, prompt }); - const text = stripCodeFences(result.text); - fs.writeFileSync(filePath, text); -} - -// --- Email style processing --- - -async function updateEmailStyle( - emailFiles: { path: string; content: string }[], - userName: string, - userEmail: string, -): Promise { - let sourceContent = `Emails written by ${userName}:\n\n`; - for (const file of emailFiles) { - const userParts = extractUserPartsFromEmail(file.content, userEmail); - if (userParts) { - sourceContent += `---\n${userParts}\n---\n\n`; - } - } - - await updateNoteFile( - NOTE_FILES.emailStyle, - 'Email writing style patterns — voice, tone, formatting, sign-offs, bucketed by recipient context. Include concrete examples.', - sourceContent, - ); - - await updateNoteFile( - NOTE_FILES.writingStyle, - 'General voice and tone patterns across all writing', - sourceContent, - ); -} - -// --- Inbox processing --- - -async function processInbox(entries: InboxEntry[]): Promise { - if (entries.length === 0) { - return 0; - } - - // Group entries by category - const grouped = new Map(); - for (const entry of entries) { - const category = entry.category; - if (!grouped.has(category)) { - grouped.set(category, []); - } - grouped.get(category)!.push(entry); - } - - // Update each relevant note file - for (const [category, categoryEntries] of grouped) { - const targetFiles = CATEGORY_TO_FILE[category]; - if (!targetFiles) { - console.log(`[AgentNotes] Unknown category: ${category}, skipping`); - continue; - } - - const sourceContent = `Observations from conversations:\n\n${categoryEntries.map(e => `- ${e.note}`).join('\n')}`; - - for (const targetFile of targetFiles) { - const description = targetFile === NOTE_FILES.preferences - ? 'Hard rules and explicit preferences — always loaded for context' - : targetFile === NOTE_FILES.writingStyle - ? 'General voice and tone patterns across all writing' - : targetFile === NOTE_FILES.people - ? 'Per-person relationship context, tone preferences, and interaction notes' - : 'Scheduling patterns, workflow habits, recurring tasks'; - - await updateNoteFile(targetFile, description, sourceContent); - } - } - - return entries.length; -} - // --- Copilot run scanning --- function findNewCopilotRuns(state: AgentNotesState): string[] { @@ -320,15 +144,10 @@ function findNewCopilotRuns(state: AgentNotesState): string[] { } } - // Sort chronologically (filenames are timestamps), newest last results.sort(); return results; } -/** - * Extract only user and assistant text messages from a run file. - * Skips tool calls, tool results, system messages, and any non-text content. - */ function extractConversationMessages(runFilePath: string): { role: string; text: string }[] { const messages: { role: string; text: string }[] = []; try { @@ -347,7 +166,6 @@ function extractConversationMessages(runFilePath: string): { role: string; text: if (typeof msg.content === 'string') { text = msg.content.trim(); } else if (Array.isArray(msg.content)) { - // Only extract text parts, skip tool-call parts text = msg.content .filter((p: { type: string }) => p.type === 'text') .map((p: { text: string }) => p.text) @@ -368,85 +186,17 @@ function extractConversationMessages(runFilePath: string): { role: string; text: return messages; } -/** - * Process copilot runs and append new facts to user.md. - * Each fact is a timestamped line. The LLM decides what's new vs already known. - */ -async function updateUserNotes(runFiles: string[]): Promise { - if (runFiles.length === 0) { - return 0; - } +// --- Wait for agent run completion --- - // Collect conversations from runs (limit to RUNS_BATCH_SIZE) - const runsToProcess = runFiles.slice(-RUNS_BATCH_SIZE); - let conversationText = ''; - - for (const runFile of runsToProcess) { - const messages = extractConversationMessages(path.join(RUNS_DIR, runFile)); - if (messages.length === 0) continue; - - conversationText += `\n--- Conversation ---\n`; - for (const msg of messages) { - conversationText += `${msg.role}: ${msg.text}\n\n`; - } - } - - if (!conversationText.trim()) { - return 0; - } - - const model = await getModel(); - const existing = readNoteFile(NOTE_FILES.user); - const timestamp = new Date().toISOString(); - - const system = `You analyze conversations between a user and their AI assistant to learn facts about the user. - -Your job: extract any new, non-trivial facts about the user that are worth remembering long-term. - -Examples of good facts: -- Working on Project X, an AI assistant product -- Team is 4 people, co-founder is Ramnique -- Preparing for Series A fundraise -- Based in Bangalore, India -- Prefers to work late evenings -- Has a meeting with Brad from Smash Capital next week - -Examples of things NOT to extract: -- Ephemeral task details ("user asked to draft an email") -- Facts the assistant already knows from tools/knowledge graph -- Obvious or trivial observations ("user uses a computer") - -Output format: Return ONLY new facts as a bullet list, one per line. Each line should be: -- [${timestamp}] The fact - -If there are no new facts worth noting, return exactly: NO_NEW_FACTS - -IMPORTANT: Check the existing user notes below. Do NOT repeat facts that are already captured there (even if worded differently).`; - - const prompt = `## Existing user notes: -${existing || '(none yet)'} - -## Recent conversations to analyze: -${conversationText} - -Extract new facts (or return NO_NEW_FACTS):`; - - const result = await generateText({ model, system, prompt }); - const text = stripCodeFences(result.text).trim(); - - if (text === 'NO_NEW_FACTS' || !text) { - return 0; - } - - // Append new facts to user.md - const header = existing ? '' : '# User\n\n'; - const newContent = existing - ? existing.trimEnd() + '\n' + text + '\n' - : header + text + '\n'; - fs.writeFileSync(NOTE_FILES.user, newContent); - - // Count lines added - return text.split('\n').filter(l => l.trim().startsWith('-')).length; +async function waitForRunCompletion(runId: string): Promise { + return new Promise(async (resolve) => { + const unsubscribe = await bus.subscribe('*', async (event) => { + if (event.type === 'run-processing-end' && event.runId === runId) { + unsubscribe(); + resolve(); + } + }); + }); } // --- Main processing --- @@ -461,147 +211,112 @@ async function processAgentNotes(): Promise { ensureAgentNotesDir(); const state = loadAgentNotesState(); - const run = await serviceLogger.startRun({ + // Collect all source material + const messageParts: string[] = []; + + // 1. Emails + const emailPaths = findUserSentEmails(state, userConfig.email, EMAIL_BATCH_SIZE); + if (emailPaths.length > 0) { + messageParts.push(`## Emails sent by ${userConfig.name}\n`); + for (const p of emailPaths) { + const content = fs.readFileSync(p, 'utf-8'); + const userParts = extractUserPartsFromEmail(content, userConfig.email); + if (userParts) { + messageParts.push(`---\n${userParts}\n---\n`); + } + } + } + + // 2. Inbox entries + const inboxEntries = readInbox(); + if (inboxEntries.length > 0) { + messageParts.push(`## Notes from the assistant (save-to-memory inbox)\n`); + messageParts.push(inboxEntries.join('\n')); + } + + // 3. Copilot conversations + const newRuns = findNewCopilotRuns(state); + const runsToProcess = newRuns.slice(-RUNS_BATCH_SIZE); + if (runsToProcess.length > 0) { + let conversationText = ''; + for (const runFile of runsToProcess) { + const messages = extractConversationMessages(path.join(RUNS_DIR, runFile)); + if (messages.length === 0) continue; + conversationText += `\n--- Conversation ---\n`; + for (const msg of messages) { + conversationText += `${msg.role}: ${msg.text}\n\n`; + } + } + if (conversationText.trim()) { + messageParts.push(`## Recent copilot conversations\n${conversationText}`); + } + } + + // Nothing to process + if (messageParts.length === 0) { + return; + } + + const serviceRun = await serviceLogger.startRun({ service: 'agent_notes', message: 'Processing agent notes', trigger: 'timer', }); - let hadError = false; - let emailsProcessed = 0; - let inboxProcessed = 0; - let userFactsAdded = 0; - - // --- Email Style Learning --- try { - const emailPaths = findUserSentEmails(state, userConfig.email, EMAIL_BATCH_SIZE); - if (emailPaths.length > 0) { - console.log(`[AgentNotes] Found ${emailPaths.length} new emails with user content`); - await serviceLogger.log({ - type: 'progress', - service: run.service, - runId: run.runId, - level: 'info', - message: `Analyzing ${emailPaths.length} emails for style`, - step: 'email_style', - current: 1, - total: 3, - }); + const timestamp = new Date().toISOString(); + const message = `Current timestamp: ${timestamp}\n\nProcess the following source material and update the agent-notes folder accordingly.\n\n${messageParts.join('\n\n')}`; - const emailFiles = emailPaths.map(p => ({ - path: p, - content: fs.readFileSync(p, 'utf-8'), - })); + const agentRun = await createRun({ agentId: AGENT_ID }); + await createMessage(agentRun.id, message); + await waitForRunCompletion(agentRun.id); - await updateEmailStyle(emailFiles, userConfig.name, userConfig.email); - - for (const p of emailPaths) { - markEmailProcessed(p, state); - } - saveAgentNotesState(state); - emailsProcessed = emailPaths.length; + // Mark everything as processed + for (const p of emailPaths) { + markEmailProcessed(p, state); } - } catch (error) { - hadError = true; - console.error('[AgentNotes] Error processing emails:', error); - await serviceLogger.log({ - type: 'error', - service: run.service, - runId: run.runId, - level: 'error', - message: 'Error processing email style', - error: error instanceof Error ? error.message : String(error), - }); - } - - // --- Inbox Processing --- - try { - const entries = readInbox(); - if (entries.length > 0) { - console.log(`[AgentNotes] Found ${entries.length} inbox entries`); - await serviceLogger.log({ - type: 'progress', - service: run.service, - runId: run.runId, - level: 'info', - message: `Processing ${entries.length} inbox entries`, - step: 'inbox', - current: 2, - total: 3, - }); - - inboxProcessed = await processInbox(entries); + for (const r of newRuns) { + markRunProcessed(r, state); + } + if (inboxEntries.length > 0) { clearInbox(); } + + state.lastRunTime = new Date().toISOString(); + saveAgentNotesState(state); + + await serviceLogger.log({ + type: 'run_complete', + service: serviceRun.service, + runId: serviceRun.runId, + level: 'info', + message: 'Agent notes processing complete', + durationMs: Date.now() - serviceRun.startedAt, + outcome: 'ok', + summary: { + emails: emailPaths.length, + inboxEntries: inboxEntries.length, + copilotRuns: runsToProcess.length, + }, + }); } catch (error) { - hadError = true; - console.error('[AgentNotes] Error processing inbox:', error); + console.error('[AgentNotes] Error processing:', error); await serviceLogger.log({ type: 'error', - service: run.service, - runId: run.runId, + service: serviceRun.service, + runId: serviceRun.runId, level: 'error', - message: 'Error processing inbox', + message: 'Error processing agent notes', error: error instanceof Error ? error.message : String(error), }); } - - // --- Copilot Run Learning (user.md) --- - try { - const newRuns = findNewCopilotRuns(state); - if (newRuns.length > 0) { - console.log(`[AgentNotes] Found ${newRuns.length} new copilot runs`); - await serviceLogger.log({ - type: 'progress', - service: run.service, - runId: run.runId, - level: 'info', - message: `Analyzing ${newRuns.length} copilot runs for user facts`, - step: 'copilot_runs', - current: 3, - total: 3, - }); - - userFactsAdded = await updateUserNotes(newRuns); - - for (const r of newRuns) { - markRunProcessed(r, state); - } - saveAgentNotesState(state); - } - } catch (error) { - hadError = true; - console.error('[AgentNotes] Error processing copilot runs:', error); - await serviceLogger.log({ - type: 'error', - service: run.service, - runId: run.runId, - level: 'error', - message: 'Error processing copilot runs', - error: error instanceof Error ? error.message : String(error), - }); - } - - state.lastRunTime = new Date().toISOString(); - saveAgentNotesState(state); - - await serviceLogger.log({ - type: 'run_complete', - service: run.service, - runId: run.runId, - level: hadError ? 'error' : 'info', - message: 'Agent notes processing complete', - durationMs: Date.now() - run.startedAt, - outcome: hadError ? 'error' : 'ok', - summary: { emailsProcessed, inboxProcessed, userFactsAdded }, - }); } // --- Entry point --- export async function init() { console.log('[AgentNotes] Starting Agent Notes Service...'); - console.log(`[AgentNotes] Will process every ${SYNC_INTERVAL_MS / 60000} minutes`); + console.log(`[AgentNotes] Will process every ${SYNC_INTERVAL_MS / 1000} seconds`); // Initial run await processAgentNotes(); diff --git a/apps/x/packages/core/src/knowledge/agent_notes_agent.ts b/apps/x/packages/core/src/knowledge/agent_notes_agent.ts new file mode 100644 index 00000000..2a1a39fd --- /dev/null +++ b/apps/x/packages/core/src/knowledge/agent_notes_agent.ts @@ -0,0 +1,53 @@ +export function getRaw(): string { + return `--- +tools: + workspace-writeFile: + type: builtin + name: workspace-writeFile + workspace-readFile: + type: builtin + name: workspace-readFile + workspace-edit: + type: builtin + name: workspace-edit + workspace-readdir: + type: builtin + name: workspace-readdir + workspace-mkdir: + type: builtin + name: workspace-mkdir +--- +# Agent Notes + +You are the agent-notes agent. You maintain a set of notes about the user in the \`knowledge/agent-notes/\` folder. Your job is to process new source material and update the notes accordingly. + +## Folder Structure + +The agent-notes folder contains markdown files that capture what you've learned about the user: + +- **user.md** — Facts about the user: who they are, what they're working on, their team, their context. Each fact is a timestamped bullet point. +- **preferences.md** — General preferences and rules (e.g., "don't use em-dashes", "no meetings before 11am"). These are injected into the assistant's system prompt on every chat. +- **style/email.md** — Email writing style patterns, bucketed by recipient context, with examples. +- Other files as needed — If you notice preferences specific to a topic (e.g., presentations, meeting prep), create a dedicated file for them (e.g., \`presentations.md\`, \`meeting-prep.md\`). + +## How to Process Source Material + +You will receive a message containing some combination of: +1. **Emails sent by the user** — Analyze their writing style and update \`style/email.md\` +2. **Inbox entries** — Notes the assistant saved during conversations via save-to-memory. Route each to the appropriate file. General preferences go to \`preferences.md\`. Topic-specific preferences get their own file. +3. **Copilot conversations** — User and assistant messages from recent chats. Extract facts about the user and append timestamped entries to \`user.md\`. + +## Rules + +- Always read a file before updating it so you know what's already there. +- For \`user.md\`: append new timestamped facts. Do NOT rewrite or remove existing entries. Format: \`- [ISO_TIMESTAMP] The fact\` +- For \`preferences.md\` and other preference files: you may reorganize and deduplicate, but preserve all existing preferences that are still relevant. +- For \`style/email.md\`: organize by recipient context (close team, investors/external, formal/cold). Include concrete examples from the emails. +- Do NOT add facts that are already captured (even if worded differently). +- Do NOT extract ephemeral task details ("user asked to draft an email"). +- Be concise — bullet points, not paragraphs. +- Capture context, not blanket rules. BAD: "User prefers casual tone". GOOD: "User prefers casual tone with internal team but formal with investors." +- If there's nothing new to add, don't modify files unnecessarily. +- Create the \`style/\` directory if it doesn't exist yet. +`; +}