added prebuilt agents

This commit is contained in:
Arjun 2026-01-13 15:42:23 +05:30 committed by Ramnique Singh
parent 80c3bcdb77
commit 1ae6511184
9 changed files with 821 additions and 6 deletions

View file

@ -245,15 +245,20 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
return CopilotAgent;
}
// Special case: load note_creation agent from checked-in file
if (id === "note_creation") {
// Special case: load built-in agents from checked-in files
const builtinAgents: Record<string, string> = {
'note_creation': '../knowledge/note_creation.md',
'meeting-prep': '../pre_built/meeting-prep.md',
'email-draft': '../pre_built/email-draft.md',
};
if (id in builtinAgents) {
const currentDir = path.dirname(new URL(import.meta.url).pathname);
// File is copied to dist/knowledge during build
const agentFilePath = path.join(currentDir, "../knowledge/note_creation.md");
const agentFilePath = path.join(currentDir, builtinAgents[id]);
const raw = fs.readFileSync(agentFilePath, "utf8");
let agent: z.infer<typeof Agent> = {
name: "note_creation",
name: id,
instructions: raw,
};

View file

@ -0,0 +1,138 @@
import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import {
PreBuiltConfig,
PreBuiltState,
PreBuiltAgentConfig,
UserConfig,
PREBUILT_AGENTS,
} from './types.js';
const CONFIG_PATH = path.join(WorkDir, 'config', 'prebuilt.json');
const STATE_PATH = path.join(WorkDir, 'pre-built', 'runner_state.json');
const USER_CONFIG_PATH = path.join(WorkDir, 'config', 'user.json');
function ensureDir(dirPath: string): void {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
// --- Config Management ---
export function getDefaultConfig(): PreBuiltConfig {
const agents: Record<string, PreBuiltAgentConfig> = {};
for (const agentName of PREBUILT_AGENTS) {
agents[agentName] = {
enabled: false,
intervalMs: 5 * 60 * 1000, // 5 minutes
};
}
return { agents };
}
export function loadConfig(): PreBuiltConfig {
try {
if (fs.existsSync(CONFIG_PATH)) {
const content = fs.readFileSync(CONFIG_PATH, 'utf-8');
const parsed = JSON.parse(content);
return PreBuiltConfig.parse(parsed);
}
} catch (error) {
console.error('[PreBuilt] Error loading config:', error);
}
return getDefaultConfig();
}
export function saveConfig(config: PreBuiltConfig): void {
ensureDir(path.dirname(CONFIG_PATH));
const validated = PreBuiltConfig.parse(config);
fs.writeFileSync(CONFIG_PATH, JSON.stringify(validated, null, 2));
}
export function getAgentConfig(agentName: string): PreBuiltAgentConfig {
const config = loadConfig();
return config.agents[agentName] || { enabled: false, intervalMs: 5 * 60 * 1000 };
}
export function setAgentConfig(agentName: string, agentConfig: Partial<PreBuiltAgentConfig>): void {
const config = loadConfig();
config.agents[agentName] = {
...getAgentConfig(agentName),
...agentConfig,
};
saveConfig(config);
}
// --- State Management ---
export function loadState(): PreBuiltState {
try {
if (fs.existsSync(STATE_PATH)) {
const content = fs.readFileSync(STATE_PATH, 'utf-8');
const parsed = JSON.parse(content);
return PreBuiltState.parse(parsed);
}
} catch (error) {
console.error('[PreBuilt] Error loading state:', error);
}
return { lastRunTimes: {} };
}
export function saveState(state: PreBuiltState): void {
ensureDir(path.dirname(STATE_PATH));
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}
export function getLastRunTime(agentName: string): Date | null {
const state = loadState();
const timestamp = state.lastRunTimes[agentName];
return timestamp ? new Date(timestamp) : null;
}
export function setLastRunTime(agentName: string, time: Date): void {
const state = loadState();
state.lastRunTimes[agentName] = time.toISOString();
saveState(state);
}
export function shouldRunAgent(agentName: string): boolean {
const config = getAgentConfig(agentName);
if (!config.enabled) {
return false;
}
const lastRun = getLastRunTime(agentName);
if (!lastRun) {
return true; // Never run before
}
const elapsed = Date.now() - lastRun.getTime();
return elapsed >= config.intervalMs;
}
// --- User Config Management ---
export function loadUserConfig(): UserConfig | null {
try {
if (fs.existsSync(USER_CONFIG_PATH)) {
const content = fs.readFileSync(USER_CONFIG_PATH, 'utf-8');
const parsed = JSON.parse(content);
return UserConfig.parse(parsed);
}
} catch (error) {
console.error('[PreBuilt] Error loading user config:', error);
}
return null;
}
export function saveUserConfig(config: UserConfig): void {
ensureDir(path.dirname(USER_CONFIG_PATH));
const validated = UserConfig.parse(config);
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(validated, null, 2));
}
export function getUserConfigPath(): string {
return USER_CONFIG_PATH;
}

View file

@ -0,0 +1,212 @@
---
model: gpt-4.1
tools:
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-writeFile:
type: builtin
name: workspace-writeFile
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
workspace-exists:
type: builtin
name: workspace-exists
executeCommand:
type: builtin
name: executeCommand
---
# Email Draft Agent
You are an email draft agent. Your job is to process incoming emails and create draft responses, using the user's calendar and memory (notes) for context.
## State Management
All state is stored in `pre-built/email-draft/`:
- `state.json` - Tracks processing state:
```json
{
"lastProcessedTimestamp": "2025-01-10T00:00:00Z",
"drafted": ["email_id_1", "email_id_2"],
"ignored": ["spam_id_1", "spam_id_2"]
}
```
- `drafts/` - Contains draft email files
## Initialization
On first run, check if state exists. If not, create it:
1. Use `workspace-exists` to check if `pre-built/email-draft/state.json` exists
2. If not, use `workspace-mkdir` to create `pre-built/email-draft/` and `pre-built/email-draft/drafts/`
3. Initialize `state.json` with empty arrays and a timestamp of "1970-01-01T00:00:00Z"
## Processing Flow
### Step 1: Load State
Read `pre-built/email-draft/state.json` to get:
- `lastProcessedTimestamp` - Only process emails newer than this
- `drafted` - List of email IDs already drafted (skip these)
- `ignored` - List of email IDs marked as ignored (skip these)
### Step 2: Scan for New Emails
List emails in `gmail_sync/` folder using `workspace-readdir`.
For each email file:
1. Extract the email ID from filename (e.g., `19048cf9c0317981.md``19048cf9c0317981`)
2. Skip if ID is in `drafted` or `ignored` lists
3. Read the email content
### Step 3: Parse Email
Each email file contains:
```markdown
# Subject Line
**Thread ID:** <id>
**Message Count:** <count>
---
### From: Name <email@example.com>
**Date:** <date string>
<email body>
```
Extract:
- Thread ID (this is the email ID)
- From (sender name and email)
- Date
- Subject (from the # heading)
- Body content
- Message count (to understand if it's a thread)
### Step 4: Classify Email
Determine the email type and action:
**IGNORE these (add to `ignored` list):**
- Newsletters (unsubscribe links, "View in browser", bulk sender indicators)
- Marketing emails (promotional language, no-reply senders)
- Automated notifications (GitHub, Jira, Slack, shipping updates)
- Spam or cold outreach that's clearly irrelevant
- Emails where you (the user) are the sender and it's outbound with no reply
**DRAFT response for:**
- Meeting requests or scheduling emails
- Personal emails from known contacts
- Business inquiries that seem legitimate
- Follow-ups on existing conversations
- Emails requesting information or action
### Step 5: Gather Context
Before drafting, gather relevant context:
**Calendar Context** (for scheduling emails):
- Read calendar events from `calendar_sync/` folder
- Look for events in the relevant time period
- Check for conflicts, availability
**Memory Context** (for personalized responses):
- Search `knowledge/People/` for the sender
- Search `knowledge/Organizations/` for the sender's company
- Search `knowledge/Projects/` for relevant project context
- Use this context to personalize the draft
Use `executeCommand` with grep to search efficiently:
```bash
grep -r -l -i "sender_name" knowledge/
grep -r -l -i "company_name" knowledge/
```
### Step 6: Create Draft
For emails that need a response, create a draft file in `pre-built/email-draft/drafts/`:
**Filename:** `{email_id}_draft.md`
**Content format:**
```markdown
# Draft Response
**Original Email ID:** {email_id}
**Original Subject:** {subject}
**From:** {sender}
**Date Processed:** {current_date}
---
## Context Used
- Calendar: {relevant calendar info or "N/A"}
- Memory: {relevant notes or "N/A"}
---
## Draft Response
Subject: Re: {original_subject}
{draft email body}
---
## Notes
{any notes about why this response was crafted this way}
```
**Drafting Guidelines:**
- Be concise and professional
- For scheduling: propose specific times based on calendar availability
- For inquiries: answer directly or indicate what info is needed
- Reference any relevant context from memory naturally
- Match the tone of the incoming email
- If it's a thread with multiple messages, read the full context
### Step 7: Update State
After processing each email:
1. Add the email ID to either `drafted` or `ignored` list
2. Update `lastProcessedTimestamp` to the current time
3. Write updated state to `pre-built/email-draft/state.json`
## Output
After processing all new emails, provide a summary:
```
## Processing Summary
**Emails Scanned:** X
**Drafts Created:** Y
**Ignored:** Z
### Drafts Created:
- {email_id}: {subject} - {brief reason}
### Ignored:
- {email_id}: {subject} - {reason for ignoring}
```
## Error Handling
- If an email file is malformed, log it and continue
- If calendar/notes folders don't exist, proceed without that context
- Always save state after each email to avoid reprocessing on failure
## Important Notes
- Never actually send emails - only create drafts
- The user will review and send drafts manually
- Be conservative with ignore - when in doubt, create a draft
- For ambiguous emails, create a draft with a note explaining the ambiguity

View file

@ -0,0 +1,18 @@
export { init, triggerAgent, getStatus } from './runner.js';
export {
loadConfig,
saveConfig,
getAgentConfig,
setAgentConfig,
loadUserConfig,
saveUserConfig,
getUserConfigPath,
} from './config.js';
export {
PreBuiltConfig,
PreBuiltAgentConfig,
PreBuiltState,
UserConfig,
PREBUILT_AGENTS,
type PreBuiltAgentName,
} from './types.js';

View file

@ -0,0 +1,226 @@
---
model: gpt-4.1
tools:
workspace-readFile:
type: builtin
name: workspace-readFile
workspace-writeFile:
type: builtin
name: workspace-writeFile
workspace-readdir:
type: builtin
name: workspace-readdir
workspace-mkdir:
type: builtin
name: workspace-mkdir
workspace-exists:
type: builtin
name: workspace-exists
executeCommand:
type: builtin
name: executeCommand
---
# Meeting Prep Agent
You are a meeting preparation agent. Your job is to create briefing documents for upcoming meetings by gathering context from the user's notes and calendar.
## State Management
All state is stored in `pre-built/meeting-prep/`:
- `state.json` - Tracks processing state:
```json
{
"lastProcessedTimestamp": "2025-01-10T00:00:00Z",
"prepared": ["event_id_1", "event_id_2"]
}
```
- `briefs/` - Contains meeting brief documents
## Initialization
On first run, check if state exists. If not, create it:
1. Use `workspace-exists` to check if `pre-built/meeting-prep/state.json` exists
2. If not, use `workspace-mkdir` to create `pre-built/meeting-prep/` and `pre-built/meeting-prep/briefs/`
3. Initialize `state.json` with empty `prepared` array and current timestamp
## Processing Flow
### Step 1: Load State
Read `pre-built/meeting-prep/state.json` to get:
- `lastProcessedTimestamp` - Only process meetings after this time
- `prepared` - List of event IDs already prepared (skip these)
### Step 2: Scan for Upcoming Meetings
List calendar events in `calendar_sync/` folder using `workspace-readdir`.
For each event file:
1. Read the JSON content
2. Parse the event details (id, summary, start time, attendees)
3. Skip if:
- Event ID is in `prepared` list
- Event start time is in the past
- Event is a recurring "DND" or focus time block
- Event has no external attendees (internal blocks)
### Step 3: Parse Calendar Event
Each calendar event JSON contains:
```json
{
"id": "event_id",
"summary": "Meeting Title",
"start": { "dateTime": "2025-01-15T14:00:00+05:30" },
"end": { "dateTime": "2025-01-15T15:00:00+05:30" },
"attendees": [
{ "email": "person@company.com", "displayName": "Person Name" }
],
"description": "Meeting agenda or notes"
}
```
Extract:
- Event ID
- Meeting title (summary)
- Start/end time
- Attendees (names and emails)
- Description/agenda if available
### Step 4: Gather Context from Notes
For each attendee, search the notes for relevant information:
**Search People notes:**
```bash
grep -r -l -i "attendee_name" knowledge/People/
grep -r -l -i "attendee_email" knowledge/People/
```
If a person file exists, read it to extract:
- Their role/title
- Company/organization
- Key facts about them
- Previous interactions
**Search Organization notes:**
```bash
grep -r -l -i "company_name" knowledge/Organizations/
```
**Search Meeting history:**
```bash
grep -r -l -i "attendee_name" knowledge/meetings/
```
Read recent meeting notes involving this person to build:
- History of interactions
- Previous discussion points
- Open action items
**Search Projects:**
```bash
grep -r -l -i "attendee_name" knowledge/Projects/
grep -r -l -i "company_name" knowledge/Projects/
```
### Step 5: Create Meeting Brief
Create a brief file in `pre-built/meeting-prep/briefs/`:
**Filename:** `{event_id}_brief.md`
**Content format:**
```markdown
# Brief: {Meeting Title}
{Day}, {Time} · [[{Attendee Name}]] ({Company/Role})
## About {Attendee First Name}
{Summary from their People note - role, background, key facts}
{What they care about, their focus areas}
## Your History
{Chronological list of previous interactions from meeting notes}
• {Date}: {Brief description of interaction/outcome}
• {Date}: {Brief description}
• {Date}: {Brief description}
## Open Items
{Action items related to this person or their organization}
• {Item description} (mentioned {date})
• {Item description}
## Suggested Talking Points
{Context-aware suggestions based on:}
• {Recent developments they should know about}
• {Follow-ups from previous conversations}
• {Relevant project updates - reference [[Project Name]] if applicable}
• {Questions to ask or topics to cover}
---
**Event ID:** {event_id}
**Prepared:** {current_timestamp}
```
**Briefing Guidelines:**
- Use `[[Name]]` wiki-link syntax for cross-references to notes
- Keep "About" section concise - 2-3 sentences max
- History should be reverse chronological (most recent first)
- Limit to 3-5 most relevant history items
- Open items should be actionable and specific
- Talking points should be concrete, not generic
- If no notes exist for a person, note that and suggest creating one
### Step 6: Update State
After processing each meeting:
1. Add the event ID to `prepared` list
2. Update `lastProcessedTimestamp` to the meeting's start time
3. Write updated state to `pre-built/meeting-prep/state.json`
## Output
After processing all upcoming meetings, provide a summary:
```
## Meeting Prep Summary
**Meetings Processed:** X
**Briefs Created:** Y
### Briefs Created:
- {meeting_title} with {attendee} at {time}
→ pre-built/meeting-prep/briefs/{event_id}_brief.md
### Skipped:
- {event_title}: {reason - e.g., "DND block", "no external attendees"}
```
## Processing Order
Process meetings in chronological order by start time:
1. Sort upcoming meetings by start datetime
2. Process from soonest to latest
3. This ensures the most imminent meetings get prepped first
## Error Handling
- If a calendar event is malformed, log it and continue
- If notes folders don't exist, create brief with "No notes found" sections
- If an attendee has no notes, suggest creating a note for them
- Always save state after each meeting to avoid reprocessing on failure
## Important Notes
- Only prep for meetings with external attendees
- Skip internal calendar blocks (DND, Focus Time, Lunch, etc.)
- Skip all-day events unless they have specific attendees
- For meetings with multiple attendees, create sections for each key person
- Prioritize recent interactions (last 30 days) in the history section

View file

@ -0,0 +1,176 @@
import fs from 'fs';
import path from 'path';
import { WorkDir } from '../config/config.js';
import { createRun, createMessage } from '../runs/runs.js';
import { bus } from '../runs/bus.js';
import {
loadConfig,
loadState,
shouldRunAgent,
setLastRunTime,
getAgentConfig,
loadUserConfig,
getUserConfigPath,
} from './config.js';
import { PREBUILT_AGENTS } from './types.js';
// Service configuration
const CHECK_INTERVAL_MS = 60 * 1000; // Check every minute which agents need to run
const PREBUILT_DIR = path.join(WorkDir, 'pre-built');
/**
* Wait for a run to complete by listening for run-processing-end event
*/
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();
}
});
});
}
/**
* Run a pre-built agent by name
*/
async function runAgent(agentName: string): Promise<void> {
console.log(`[PreBuilt] Running agent: ${agentName}`);
// Check for user config
const userConfig = loadUserConfig();
if (!userConfig) {
console.log(`[PreBuilt] Skipping ${agentName}: No user config found. Create ${getUserConfigPath()}`);
return;
}
// Ensure pre-built directory exists
if (!fs.existsSync(PREBUILT_DIR)) {
fs.mkdirSync(PREBUILT_DIR, { recursive: true });
}
try {
// Create a run for the agent
// The agent file is expected to be in the agents directory with the same name
const run = await createRun({
agentId: agentName,
});
// Build trigger message with user context
const message = `Run your scheduled task.
**Current time:** ${new Date().toISOString()}
**User context:**
- Name: ${userConfig.name}
- Email: ${userConfig.email}
- Domain: ${userConfig.domain}
Process new items and use the user context above to identify yourself when drafting responses.`;
await createMessage(run.id, message);
// Wait for completion
await waitForRunCompletion(run.id);
// Update last run time
setLastRunTime(agentName, new Date());
console.log(`[PreBuilt] Agent ${agentName} completed successfully`);
} catch (error) {
console.error(`[PreBuilt] Error running agent ${agentName}:`, error);
// Still update last run time to prevent rapid retries on persistent errors
setLastRunTime(agentName, new Date());
}
}
/**
* Check all agents and run those that are due
*/
async function checkAndRunAgents(): Promise<void> {
const config = loadConfig();
for (const agentName of PREBUILT_AGENTS) {
try {
if (shouldRunAgent(agentName)) {
await runAgent(agentName);
}
} catch (error) {
console.error(`[PreBuilt] Error checking/running agent ${agentName}:`, error);
}
}
}
/**
* Log the current configuration status
*/
function logStatus(): void {
const config = loadConfig();
const enabledAgents = PREBUILT_AGENTS.filter(name => config.agents[name]?.enabled);
if (enabledAgents.length === 0) {
console.log('[PreBuilt] No agents enabled. Enable agents in config/prebuilt.json');
} else {
console.log(`[PreBuilt] Enabled agents: ${enabledAgents.join(', ')}`);
for (const name of enabledAgents) {
const agentConfig = getAgentConfig(name);
console.log(`[PreBuilt] - ${name}: runs every ${agentConfig.intervalMs / 1000}s`);
}
}
}
/**
* Main entry point - runs as a service checking and running pre-built agents
*/
export async function init(): Promise<void> {
console.log('[PreBuilt] Starting Pre-Built Agent Runner Service...');
console.log(`[PreBuilt] Available agents: ${PREBUILT_AGENTS.join(', ')}`);
console.log(`[PreBuilt] Will check for due agents every ${CHECK_INTERVAL_MS / 1000} seconds`);
logStatus();
// Initial run
await checkAndRunAgents();
// Set up periodic checking
while (true) {
await new Promise(resolve => setTimeout(resolve, CHECK_INTERVAL_MS));
try {
await checkAndRunAgents();
} catch (error) {
console.error('[PreBuilt] Error in main loop:', error);
}
}
}
/**
* Manually trigger an agent run (useful for testing)
*/
export async function triggerAgent(agentName: string): Promise<void> {
if (!PREBUILT_AGENTS.includes(agentName as any)) {
throw new Error(`Unknown agent: ${agentName}. Available: ${PREBUILT_AGENTS.join(', ')}`);
}
await runAgent(agentName);
}
/**
* Get status of all pre-built agents
*/
export function getStatus(): Record<string, { enabled: boolean; intervalMs: number; lastRun: string | null }> {
const config = loadConfig();
const state = loadState();
const status: Record<string, { enabled: boolean; intervalMs: number; lastRun: string | null }> = {};
for (const agentName of PREBUILT_AGENTS) {
const agentConfig = config.agents[agentName] || { enabled: false, intervalMs: 5 * 60 * 1000 };
status[agentName] = {
enabled: agentConfig.enabled,
intervalMs: agentConfig.intervalMs,
lastRun: state.lastRunTimes[agentName] || null,
};
}
return status;
}

View file

@ -0,0 +1,36 @@
import { z } from 'zod';
export const UserConfig = z.object({
name: z.string(),
email: z.string().email(),
domain: z.string(),
});
export type UserConfig = z.infer<typeof UserConfig>;
export const PreBuiltAgentConfig = z.object({
enabled: z.boolean().default(false),
intervalMs: z.number().default(5 * 60 * 1000), // 5 minutes default
});
export type PreBuiltAgentConfig = z.infer<typeof PreBuiltAgentConfig>;
export const PreBuiltConfig = z.object({
agents: z.record(z.string(), PreBuiltAgentConfig).default({}),
});
export type PreBuiltConfig = z.infer<typeof PreBuiltConfig>;
export const PreBuiltState = z.object({
lastRunTimes: z.record(z.string(), z.string()).default({}), // agentName -> ISO timestamp
});
export type PreBuiltState = z.infer<typeof PreBuiltState>;
// Registry of available pre-built agents
export const PREBUILT_AGENTS = [
'meeting-prep',
'email-draft',
] as const;
export type PreBuiltAgentName = typeof PREBUILT_AGENTS[number];