moved to a full agent

This commit is contained in:
Arjun 2026-03-23 21:27:07 +05:30
parent 8e55eaa613
commit 6028fda2db
5 changed files with 218 additions and 406 deletions

View file

@ -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<z.infer<typeof Agent>> {
return agent;
}
if (id === 'agent_notes_agent') {
const agentNotesAgentRaw = getAgentNotesAgentRaw();
let agent: z.infer<typeof Agent> = {
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<IAgentsRepo>('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;

View file

@ -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.

View file

@ -1261,24 +1261,23 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
},
},
'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}`,
};
},
},

View file

@ -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<string, string[]> = {
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<IModelConfigRepo>('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<void> {
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<void> {
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<number> {
if (entries.length === 0) {
return 0;
}
// Group entries by category
const grouped = new Map<string, InboxEntry[]>();
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<number> {
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<void> {
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<void> {
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();

View file

@ -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.
`;
}