@agenticmail/enterprise 0.5.166 → 0.5.168

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.
@@ -0,0 +1,727 @@
1
+ /**
2
+ * Agent Autonomy System
3
+ *
4
+ * Provides enterprise-grade autonomous behaviors:
5
+ * 1. Auto Clock-In/Out based on work schedule
6
+ * 2. Daily/Weekly Manager Catchup Emails
7
+ * 3. Goal Setting & Auto-Reminders
8
+ * 4. Friday Knowledge Contribution
9
+ * 5. Smart Answer Escalation (memory → Drive → Sites → manager)
10
+ * 6. Guardrail Rule Enforcement at runtime
11
+ */
12
+
13
+ import type { EngineDatabase } from './db-adapter.js';
14
+
15
+ // ─── Types ──────────────────────────────────────────────
16
+
17
+ export interface AutonomyConfig {
18
+ agentId: string;
19
+ orgId: string;
20
+ agentName: string;
21
+ role: string;
22
+ managerEmail?: string;
23
+ timezone: string;
24
+ schedule?: { start: string; end: string; days: number[] };
25
+ emailProvider?: any;
26
+ runtime?: any;
27
+ engineDb: EngineDatabase;
28
+ memoryManager?: any;
29
+ lifecycle?: any;
30
+ }
31
+
32
+ export interface ClockState {
33
+ clockedIn: boolean;
34
+ clockInTime?: string;
35
+ clockOutTime?: string;
36
+ lastCheckTime?: string;
37
+ }
38
+
39
+ interface CatchupData {
40
+ emailsHandled: number;
41
+ sessionsRun: number;
42
+ memoriesStored: number;
43
+ tasksCompleted: string[];
44
+ issuesEncountered: string[];
45
+ knowledgeGained: string[];
46
+ }
47
+
48
+ // ─── Agent Autonomy Manager ─────────────────────────────
49
+
50
+ export class AgentAutonomyManager {
51
+ private config: AutonomyConfig;
52
+ private clockState: ClockState = { clockedIn: false };
53
+ private schedulerInterval?: NodeJS.Timeout;
54
+ private catchupInterval?: NodeJS.Timeout;
55
+ private knowledgeInterval?: NodeJS.Timeout;
56
+ private goalCheckInterval?: NodeJS.Timeout;
57
+
58
+ constructor(config: AutonomyConfig) {
59
+ this.config = config;
60
+ }
61
+
62
+ async start(): Promise<void> {
63
+ console.log('[autonomy] Starting agent autonomy system...');
64
+
65
+ // Check clock state on boot
66
+ await this.checkClockState();
67
+
68
+ // Schedule checker runs every minute
69
+ this.schedulerInterval = setInterval(() => this.checkClockState(), 60_000);
70
+
71
+ // Catchup email checker runs every 30 minutes (checks if it's time for daily/weekly)
72
+ this.catchupInterval = setInterval(() => this.checkCatchupSchedule(), 30 * 60_000);
73
+ // Check immediately on start (after 30s delay for systems to settle)
74
+ setTimeout(() => this.checkCatchupSchedule(), 30_000);
75
+
76
+ // Knowledge contribution checker runs every hour
77
+ this.knowledgeInterval = setInterval(() => this.checkKnowledgeContribution(), 60 * 60_000);
78
+
79
+ // Goal progress checker runs every 2 hours
80
+ this.goalCheckInterval = setInterval(() => this.checkGoalProgress(), 2 * 60 * 60_000);
81
+
82
+ console.log('[autonomy] System started — clock, catchup, knowledge, goals active');
83
+ }
84
+
85
+ stop(): void {
86
+ if (this.schedulerInterval) clearInterval(this.schedulerInterval);
87
+ if (this.catchupInterval) clearInterval(this.catchupInterval);
88
+ if (this.knowledgeInterval) clearInterval(this.knowledgeInterval);
89
+ if (this.goalCheckInterval) clearInterval(this.goalCheckInterval);
90
+ console.log('[autonomy] System stopped');
91
+ }
92
+
93
+ // ─── 1. Auto Clock-In/Out ──────────────────────────
94
+
95
+ private async checkClockState(): Promise<void> {
96
+ const schedule = this.config.schedule;
97
+ if (!schedule) return;
98
+
99
+ const now = new Date();
100
+ const tz = this.config.timezone || 'UTC';
101
+ const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
102
+ const currentHour = localTime.getHours();
103
+ const currentMinute = localTime.getMinutes();
104
+ const currentDay = localTime.getDay(); // 0=Sun
105
+ const currentTimeStr = `${String(currentHour).padStart(2, '0')}:${String(currentMinute).padStart(2, '0')}`;
106
+
107
+ // Check if today is a scheduled workday
108
+ const isWorkday = schedule.days.includes(currentDay);
109
+ const isWithinHours = currentTimeStr >= schedule.start && currentTimeStr < schedule.end;
110
+
111
+ if (isWorkday && isWithinHours && !this.clockState.clockedIn) {
112
+ await this.clockIn();
113
+ } else if ((!isWorkday || !isWithinHours) && this.clockState.clockedIn) {
114
+ await this.clockOut();
115
+ }
116
+
117
+ this.clockState.lastCheckTime = now.toISOString();
118
+ }
119
+
120
+ private async clockIn(): Promise<void> {
121
+ const now = new Date().toISOString();
122
+ this.clockState.clockedIn = true;
123
+ this.clockState.clockInTime = now;
124
+
125
+ try {
126
+ await this.config.engineDb.execute(
127
+ `INSERT INTO clock_records (id, org_id, agent_id, type, triggered_by, created_at) VALUES ($1, $2, $3, 'clock_in', 'auto_scheduler', $4)`,
128
+ [crypto.randomUUID(), this.config.orgId, this.config.agentId, now]
129
+ );
130
+ console.log(`[autonomy] ⏰ Clocked IN at ${now}`);
131
+
132
+ // Store in memory
133
+ if (this.config.memoryManager) {
134
+ await this.config.memoryManager.storeMemory(this.config.agentId, {
135
+ content: `Clocked in at ${now}. Starting work shift.`,
136
+ category: 'context',
137
+ importance: 'low',
138
+ confidence: 1.0,
139
+ }).catch(() => {});
140
+ }
141
+ } catch (err: any) {
142
+ console.error(`[autonomy] Clock-in error: ${err.message}`);
143
+ }
144
+ }
145
+
146
+ private async clockOut(): Promise<void> {
147
+ const now = new Date().toISOString();
148
+ this.clockState.clockedIn = false;
149
+ this.clockState.clockOutTime = now;
150
+
151
+ try {
152
+ await this.config.engineDb.execute(
153
+ `INSERT INTO clock_records (id, org_id, agent_id, type, triggered_by, reason, created_at) VALUES ($1, $2, $3, 'clock_out', 'auto_scheduler', 'End of scheduled hours', $4)`,
154
+ [crypto.randomUUID(), this.config.orgId, this.config.agentId, now]
155
+ );
156
+ console.log(`[autonomy] ⏰ Clocked OUT at ${now}`);
157
+
158
+ // Store in memory
159
+ if (this.config.memoryManager) {
160
+ await this.config.memoryManager.storeMemory(this.config.agentId, {
161
+ content: `Clocked out at ${now}. Work shift ended.`,
162
+ category: 'context',
163
+ importance: 'low',
164
+ confidence: 1.0,
165
+ }).catch(() => {});
166
+ }
167
+ } catch (err: any) {
168
+ console.error(`[autonomy] Clock-out error: ${err.message}`);
169
+ }
170
+ }
171
+
172
+ isWorkingHours(): boolean {
173
+ return this.clockState.clockedIn;
174
+ }
175
+
176
+ getClockState(): ClockState {
177
+ return { ...this.clockState };
178
+ }
179
+
180
+ // ─── 2. Manager Catchup Emails ─────────────────────
181
+
182
+ private async checkCatchupSchedule(): Promise<void> {
183
+ if (!this.config.managerEmail || !this.config.runtime) return;
184
+
185
+ const now = new Date();
186
+ const tz = this.config.timezone || 'UTC';
187
+ const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
188
+ const hour = localTime.getHours();
189
+ const minute = localTime.getMinutes();
190
+ const dayOfWeek = localTime.getDay(); // 0=Sun, 1=Mon
191
+ const dateStr = localTime.toISOString().split('T')[0];
192
+
193
+ // Daily catchup: 9:00 AM on workdays (within first 30 min window)
194
+ const isDailyCatchupTime = hour === 9 && minute < 30;
195
+ // Weekly catchup: Monday 9:00 AM (within first 30 min window)
196
+ const isWeeklyCatchupTime = dayOfWeek === 1 && hour === 9 && minute < 30;
197
+
198
+ if (!isDailyCatchupTime) return;
199
+
200
+ // Check if we already sent today's catchup
201
+ const catchupKey = isWeeklyCatchupTime ? `weekly_catchup_${dateStr}` : `daily_catchup_${dateStr}`;
202
+ const alreadySent = await this.checkMemoryFlag(catchupKey);
203
+ if (alreadySent) return;
204
+
205
+ console.log(`[autonomy] ${isWeeklyCatchupTime ? 'Weekly' : 'Daily'} catchup time — generating report...`);
206
+
207
+ try {
208
+ const catchupData = await this.gatherCatchupData(isWeeklyCatchupTime ? 7 : 1);
209
+ await this.sendCatchupEmail(catchupData, isWeeklyCatchupTime);
210
+ await this.setMemoryFlag(catchupKey);
211
+ } catch (err: any) {
212
+ console.error(`[autonomy] Catchup email error: ${err.message}`);
213
+ }
214
+ }
215
+
216
+ private async gatherCatchupData(daysBack: number): Promise<CatchupData> {
217
+ const since = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1000).toISOString();
218
+ const db = this.config.engineDb;
219
+ const agentId = this.config.agentId;
220
+
221
+ // Count processed emails
222
+ let emailsHandled = 0;
223
+ try {
224
+ const rows = await db.query<any>(
225
+ `SELECT COUNT(*) as cnt FROM agent_memory WHERE agent_id = $1 AND category = 'processed_email' AND created_at >= $2`,
226
+ [agentId, since]
227
+ );
228
+ emailsHandled = rows?.[0]?.cnt || 0;
229
+ } catch {}
230
+
231
+ // Count sessions
232
+ let sessionsRun = 0;
233
+ try {
234
+ const rows = await db.query<any>(
235
+ `SELECT COUNT(*) as cnt FROM agent_sessions WHERE agent_id = $1 AND created_at >= $2`,
236
+ [agentId, since]
237
+ );
238
+ sessionsRun = rows?.[0]?.cnt || 0;
239
+ } catch {}
240
+
241
+ // Count memories stored
242
+ let memoriesStored = 0;
243
+ try {
244
+ const rows = await db.query<any>(
245
+ `SELECT COUNT(*) as cnt FROM agent_memory WHERE agent_id = $1 AND created_at >= $2`,
246
+ [agentId, since]
247
+ );
248
+ memoriesStored = rows?.[0]?.cnt || 0;
249
+ } catch {}
250
+
251
+ // Get key tasks completed (from Google Tasks memories)
252
+ let tasksCompleted: string[] = [];
253
+ try {
254
+ const taskRows = await db.query<any>(
255
+ `SELECT content FROM agent_memory WHERE agent_id = $1 AND category = 'skill' AND content LIKE '%task%complete%' AND created_at >= $2 ORDER BY created_at DESC LIMIT 10`,
256
+ [agentId, since]
257
+ );
258
+ tasksCompleted = (taskRows || []).map((r: any) => r.content?.slice(0, 200));
259
+ } catch {}
260
+
261
+ // Get issues encountered (corrections/errors)
262
+ let issuesEncountered: string[] = [];
263
+ try {
264
+ const issueRows = await db.query<any>(
265
+ `SELECT content FROM agent_memory WHERE agent_id = $1 AND category = 'correction' AND created_at >= $2 ORDER BY created_at DESC LIMIT 5`,
266
+ [agentId, since]
267
+ );
268
+ issuesEncountered = (issueRows || []).map((r: any) => r.content?.slice(0, 200));
269
+ } catch {}
270
+
271
+ // Get knowledge gained
272
+ let knowledgeGained: string[] = [];
273
+ try {
274
+ const knowRows = await db.query<any>(
275
+ `SELECT content FROM agent_memory WHERE agent_id = $1 AND (category = 'skill' OR category = 'org_knowledge') AND created_at >= $2 ORDER BY created_at DESC LIMIT 10`,
276
+ [agentId, since]
277
+ );
278
+ knowledgeGained = (knowRows || []).map((r: any) => r.content?.slice(0, 200));
279
+ } catch {}
280
+
281
+ return { emailsHandled, sessionsRun, memoriesStored, tasksCompleted, issuesEncountered, knowledgeGained };
282
+ }
283
+
284
+ private async sendCatchupEmail(data: CatchupData, isWeekly: boolean): Promise<void> {
285
+ const runtime = this.config.runtime;
286
+ const managerEmail = this.config.managerEmail;
287
+ const agentName = this.config.agentName;
288
+ const role = this.config.role;
289
+ const period = isWeekly ? 'last week' : 'yesterday';
290
+ const nextPeriod = isWeekly ? 'this week' : 'today';
291
+
292
+ const prompt = `You need to send your ${isWeekly ? 'weekly' : 'daily'} catchup email to your manager at ${managerEmail}.
293
+
294
+ Here's what you accomplished ${period}:
295
+ - Emails handled: ${data.emailsHandled}
296
+ - Sessions/conversations: ${data.sessionsRun}
297
+ - Memories stored: ${data.memoriesStored}
298
+ - Tasks completed: ${data.tasksCompleted.length > 0 ? data.tasksCompleted.join('; ') : 'None tracked'}
299
+ - Issues encountered: ${data.issuesEncountered.length > 0 ? data.issuesEncountered.join('; ') : 'None'}
300
+ - Knowledge gained: ${data.knowledgeGained.length > 0 ? data.knowledgeGained.join('; ') : 'None tracked'}
301
+
302
+ Write and send a concise, professional ${isWeekly ? 'weekly' : 'daily'} summary email. Include:
303
+ 1. What you accomplished ${period} (be specific, not generic)
304
+ 2. Any issues or blockers you encountered
305
+ 3. What you plan to focus on ${nextPeriod}
306
+ ${isWeekly ? '4. Goals for the week (create Google Tasks for each goal)' : ''}
307
+ ${isWeekly ? '5. Any suggestions for improvement or areas where you need guidance' : ''}
308
+
309
+ Keep it under ${isWeekly ? '400' : '250'} words. Be genuine and specific — your manager reads these to stay informed.
310
+ Use gmail_send to send the email. Subject: "${isWeekly ? 'Weekly' : 'Daily'} Update — ${agentName}"
311
+
312
+ ${isWeekly ? 'After sending the email, create Google Tasks for your goals this week using google_tasks_create.' : ''}`;
313
+
314
+ const systemPrompt = `You are ${agentName}, a ${role}. You are sending your ${isWeekly ? 'weekly' : 'daily'} catchup email to your manager.
315
+ Be professional but genuine. Use real data from the summary — don't make up accomplishments.
316
+ Available tools: gmail_send (to, subject, body), google_tasks_create (listId, title, notes, dueDate).`;
317
+
318
+ try {
319
+ const session = await runtime.spawnSession({
320
+ agentId: this.config.agentId,
321
+ message: prompt,
322
+ systemPrompt,
323
+ });
324
+ console.log(`[autonomy] ✅ ${isWeekly ? 'Weekly' : 'Daily'} catchup email session ${session.id} created`);
325
+ } catch (err: any) {
326
+ console.error(`[autonomy] Failed to send catchup email: ${err.message}`);
327
+ }
328
+ }
329
+
330
+ // ─── 3. Goal Setting & Auto-Reminders ──────────────
331
+
332
+ private async checkGoalProgress(): Promise<void> {
333
+ if (!this.config.runtime || !this.clockState.clockedIn) return;
334
+
335
+ const now = new Date();
336
+ const tz = this.config.timezone || 'UTC';
337
+ const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
338
+ const hour = localTime.getHours();
339
+
340
+ // Check goals at 2 PM and 5 PM (end-of-day review)
341
+ if (hour !== 14 && hour !== 17) return;
342
+
343
+ const dateStr = localTime.toISOString().split('T')[0];
344
+ const checkKey = `goal_check_${dateStr}_${hour}`;
345
+ const alreadyChecked = await this.checkMemoryFlag(checkKey);
346
+ if (alreadyChecked) return;
347
+
348
+ console.log(`[autonomy] Goal progress check at ${hour}:00`);
349
+
350
+ try {
351
+ const prompt = hour === 17
352
+ ? `It's end of day. Review your goals and tasks:
353
+ 1. Call google_tasks_list to see your current tasks
354
+ 2. Review what you completed today
355
+ 3. Mark completed tasks as done with google_tasks_complete
356
+ 4. For incomplete tasks, update notes with progress
357
+ 5. Store a brief end-of-day reflection in memory about what went well and what to improve tomorrow
358
+ 6. If any task is blocked, email your manager at ${this.config.managerEmail || 'your manager'} about it`
359
+ : `Mid-day goal check:
360
+ 1. Call google_tasks_list to see your current tasks
361
+ 2. Review progress on today's priorities
362
+ 3. If you're behind on any task, adjust your approach
363
+ 4. Store any insights in memory for future reference`;
364
+
365
+ const session = await this.config.runtime.spawnSession({
366
+ agentId: this.config.agentId,
367
+ message: prompt,
368
+ systemPrompt: `You are ${this.config.agentName}, a ${this.config.role}. You are doing a ${hour === 17 ? 'end-of-day' : 'mid-day'} goal review. Be thorough but efficient.`,
369
+ });
370
+ console.log(`[autonomy] ✅ Goal check session ${session.id} created`);
371
+ await this.setMemoryFlag(checkKey);
372
+ } catch (err: any) {
373
+ console.error(`[autonomy] Goal check error: ${err.message}`);
374
+ }
375
+ }
376
+
377
+ // ─── 4. Knowledge Contribution (Friday) ────────────
378
+
379
+ private async checkKnowledgeContribution(): Promise<void> {
380
+ if (!this.config.runtime || !this.clockState.clockedIn) return;
381
+
382
+ const now = new Date();
383
+ const tz = this.config.timezone || 'UTC';
384
+ const localTime = new Date(now.toLocaleString('en-US', { timeZone: tz }));
385
+ const dayOfWeek = localTime.getDay(); // 5=Friday
386
+ const hour = localTime.getHours();
387
+ const dateStr = localTime.toISOString().split('T')[0];
388
+
389
+ // Friday at 3 PM
390
+ if (dayOfWeek !== 5 || hour !== 15) return;
391
+
392
+ const contribKey = `knowledge_contribution_${dateStr}`;
393
+ const alreadyDone = await this.checkMemoryFlag(contribKey);
394
+ if (alreadyDone) return;
395
+
396
+ console.log('[autonomy] Friday knowledge contribution time!');
397
+
398
+ try {
399
+ // Determine role-based category
400
+ const roleCategory = this.mapRoleToKnowledgeCategory(this.config.role);
401
+
402
+ const prompt = `It's Friday — time for your weekly knowledge contribution.
403
+
404
+ Your role is ${this.config.role}, so focus on ${roleCategory} knowledge.
405
+
406
+ Steps:
407
+ 1. Search your memory for key learnings this week: memory(action: "search", query: "learned this week")
408
+ 2. Search for tool patterns you discovered: memory(action: "search", query: "tool")
409
+ 3. Search for corrections and gotchas: memory(action: "search", query: "correction")
410
+ 4. Compile the most valuable learnings into knowledge entries
411
+ 5. For each entry, store it with a clear title and category:
412
+ - memory(action: "set", key: "knowledge-contrib-[topic]", value: "Clear description of the learning, including steps and examples", category: "org_knowledge", importance: "high")
413
+
414
+ Categories to contribute to (pick the most relevant):
415
+ ${roleCategory === 'support' ? '- customer-issues: Common customer problems and solutions\n- escalation-procedures: When and how to escalate\n- tool-patterns: Efficient ways to use tools\n- communication-templates: Effective response patterns' : ''}
416
+ ${roleCategory === 'sales' ? '- objection-handling: How to address common objections\n- product-knowledge: Product features and benefits\n- prospect-research: Effective research methods' : ''}
417
+ ${roleCategory === 'engineering' ? '- debugging-patterns: Common issues and fixes\n- architecture-decisions: Design choices and rationale\n- tool-expertise: Development tool tips' : ''}
418
+ ${roleCategory === 'general' ? '- best-practices: General workflow improvements\n- tool-patterns: Tool usage tips\n- process-improvements: Better ways to do things' : ''}
419
+
420
+ After storing knowledge entries, email your manager a brief summary of what you contributed.
421
+ Aim for 3-5 high-quality entries. Quality over quantity.`;
422
+
423
+ const session = await this.config.runtime.spawnSession({
424
+ agentId: this.config.agentId,
425
+ message: prompt,
426
+ systemPrompt: `You are ${this.config.agentName}, a ${this.config.role}. You are contributing weekly knowledge to your organization's knowledge base. Focus on ${roleCategory}-related insights. Be specific and actionable — vague entries are useless.`,
427
+ });
428
+ console.log(`[autonomy] ✅ Knowledge contribution session ${session.id} created`);
429
+ await this.setMemoryFlag(contribKey);
430
+ } catch (err: any) {
431
+ console.error(`[autonomy] Knowledge contribution error: ${err.message}`);
432
+ }
433
+ }
434
+
435
+ private mapRoleToKnowledgeCategory(role: string): string {
436
+ const roleLower = (role || '').toLowerCase();
437
+ if (roleLower.includes('support') || roleLower.includes('customer') || roleLower.includes('service')) return 'support';
438
+ if (roleLower.includes('sales') || roleLower.includes('business dev')) return 'sales';
439
+ if (roleLower.includes('engineer') || roleLower.includes('developer') || roleLower.includes('technical')) return 'engineering';
440
+ if (roleLower.includes('marketing') || roleLower.includes('content')) return 'marketing';
441
+ if (roleLower.includes('hr') || roleLower.includes('human resource') || roleLower.includes('people')) return 'hr';
442
+ if (roleLower.includes('finance') || roleLower.includes('accounting')) return 'finance';
443
+ if (roleLower.includes('legal') || roleLower.includes('compliance')) return 'legal';
444
+ if (roleLower.includes('research') || roleLower.includes('analyst')) return 'research';
445
+ if (roleLower.includes('operations') || roleLower.includes('ops')) return 'operations';
446
+ return 'general';
447
+ }
448
+
449
+ // ─── 5. Smart Answer Escalation ────────────────────
450
+
451
+ /**
452
+ * Generates a system prompt addendum that teaches the agent the escalation workflow.
453
+ * This is injected into every email-handling session.
454
+ */
455
+ static getEscalationPrompt(managerEmail: string | undefined, orgDriveFolderId?: string): string {
456
+ return `
457
+ == SMART ANSWER WORKFLOW (MANDATORY) ==
458
+ When you receive a question or request you're not 100% confident about, follow this escalation chain:
459
+
460
+ STEP 1: Search your own memory
461
+ - memory(action: "search", query: "relevant keywords")
462
+ - Check for similar past questions, corrections, and learned patterns
463
+
464
+ STEP 2: Search organization Drive (shared knowledge)
465
+ ${orgDriveFolderId ? `- google_drive_list with query "fullText contains 'search terms'" and parents in '${orgDriveFolderId}'` : '- google_drive_list with query "fullText contains \'search terms\'" to search shared docs'}
466
+ - Read relevant documents with google_drive_get to find the answer
467
+ - Check Google Sheets for data tables, Google Docs for procedures
468
+
469
+ STEP 3: If still unsure — ESCALATE to manager
470
+ ${managerEmail ? `- Send an email to ${managerEmail} with:` : '- Send an email to your manager with:'}
471
+ Subject: "Need Guidance: [Brief topic]"
472
+ Body must include:
473
+ a) The original question/request (who asked, what they need)
474
+ b) What you found in your search (memory + Drive results)
475
+ c) Your proposed answer (what you THINK the answer should be)
476
+ d) What specifically you're unsure about
477
+ e) Ask for approval or correction before responding
478
+
479
+ NEVER guess or fabricate an answer. It's better to escalate than to be wrong.
480
+
481
+ After receiving manager feedback:
482
+ - Store the correct answer in memory as a "correction" or "org_knowledge" entry
483
+ - Apply the correction to your response
484
+ - Thank the requester for their patience
485
+
486
+ The goal: build confidence over time. Today you escalate often. In a month, you'll know most answers from memory.`;
487
+ }
488
+
489
+ // ─── Helper Methods ────────────────────────────────
490
+
491
+ private async checkMemoryFlag(key: string): Promise<boolean> {
492
+ if (!this.config.memoryManager) return false;
493
+ try {
494
+ const results = await this.config.memoryManager.recall(this.config.agentId, key, 1);
495
+ return results.some((m: any) => m.content?.includes(key));
496
+ } catch {
497
+ // Fallback to DB check
498
+ try {
499
+ const rows = await this.config.engineDb.query<any>(
500
+ `SELECT id FROM agent_memory WHERE agent_id = $1 AND content LIKE $2 LIMIT 1`,
501
+ [this.config.agentId, `%${key}%`]
502
+ );
503
+ return (rows && rows.length > 0);
504
+ } catch { return false; }
505
+ }
506
+ }
507
+
508
+ private async setMemoryFlag(key: string): Promise<void> {
509
+ if (!this.config.memoryManager) return;
510
+ try {
511
+ await this.config.memoryManager.storeMemory(this.config.agentId, {
512
+ content: `${key}: completed at ${new Date().toISOString()}`,
513
+ category: 'context',
514
+ importance: 'low',
515
+ confidence: 1.0,
516
+ });
517
+ } catch {}
518
+ }
519
+ }
520
+
521
+ // ─── Guardrail Runtime Enforcement ──────────────────────
522
+
523
+ /**
524
+ * Evaluates guardrail rules against a runtime event.
525
+ * Called from runtime hooks (beforeToolCall, afterToolCall, etc.)
526
+ */
527
+ export class GuardrailEnforcer {
528
+ private engineDb: EngineDatabase;
529
+ private rules: Map<string, any> = new Map();
530
+ private lastLoad = 0;
531
+ private readonly RELOAD_INTERVAL = 5 * 60_000; // reload rules every 5 min
532
+
533
+ constructor(engineDb: EngineDatabase) {
534
+ this.engineDb = engineDb;
535
+ }
536
+
537
+ private async ensureRulesLoaded(): Promise<void> {
538
+ if (Date.now() - this.lastLoad < this.RELOAD_INTERVAL && this.rules.size > 0) return;
539
+ try {
540
+ const rows = await this.engineDb.query<any>('SELECT * FROM guardrail_rules WHERE enabled = TRUE');
541
+ this.rules.clear();
542
+ for (const r of (rows || [])) {
543
+ this.rules.set(r.id, {
544
+ id: r.id, orgId: r.org_id, name: r.name, category: r.category,
545
+ ruleType: r.rule_type,
546
+ conditions: typeof r.conditions === 'string' ? JSON.parse(r.conditions) : (r.conditions || {}),
547
+ action: r.action, severity: r.severity || 'medium',
548
+ cooldownMinutes: r.cooldown_minutes || 0,
549
+ lastTriggeredAt: r.last_triggered_at, triggerCount: r.trigger_count || 0,
550
+ });
551
+ }
552
+ this.lastLoad = Date.now();
553
+ } catch (err: any) {
554
+ console.warn(`[guardrail-enforcer] Failed to load rules: ${err.message}`);
555
+ }
556
+ }
557
+
558
+ /**
559
+ * Check if an agent action should be blocked or flagged.
560
+ * Returns { allowed: true } or { allowed: false, reason, action }
561
+ */
562
+ async evaluate(event: {
563
+ agentId: string;
564
+ orgId: string;
565
+ type: 'tool_call' | 'email_send' | 'session_start' | 'memory_write';
566
+ toolName?: string;
567
+ content?: string;
568
+ metadata?: Record<string, any>;
569
+ }): Promise<{ allowed: boolean; reason?: string; action?: string; ruleId?: string }> {
570
+ await this.ensureRulesLoaded();
571
+
572
+ for (const rule of this.rules.values()) {
573
+ // Check agent scope
574
+ if (rule.conditions.agentIds?.length > 0 && !rule.conditions.agentIds.includes(event.agentId)) continue;
575
+ // Check org scope
576
+ if (rule.orgId !== event.orgId) continue;
577
+ // Check cooldown
578
+ if (rule.lastTriggeredAt && rule.cooldownMinutes > 0) {
579
+ const cooldownUntil = new Date(rule.lastTriggeredAt).getTime() + rule.cooldownMinutes * 60_000;
580
+ if (Date.now() < cooldownUntil) continue;
581
+ }
582
+
583
+ const triggered = await this.evaluateRule(rule, event);
584
+ if (triggered) {
585
+ await this.recordTrigger(rule, event, triggered);
586
+ if (rule.action === 'kill' || rule.action === 'pause') {
587
+ return { allowed: false, reason: triggered, action: rule.action, ruleId: rule.id };
588
+ }
589
+ // alert/notify/log — allow but log
590
+ console.warn(`[guardrail-enforcer] Rule "${rule.name}" triggered: ${triggered} (action: ${rule.action})`);
591
+ }
592
+ }
593
+
594
+ return { allowed: true };
595
+ }
596
+
597
+ private async evaluateRule(rule: any, event: any): Promise<string | null> {
598
+ switch (rule.ruleType) {
599
+ case 'keyword_detection': {
600
+ if (!event.content) return null;
601
+ const keywords = rule.conditions.keywords || [];
602
+ const contentLower = event.content.toLowerCase();
603
+ for (const kw of keywords) {
604
+ if (contentLower.includes(kw.toLowerCase())) {
605
+ return `Keyword detected: "${kw}" in ${event.type}`;
606
+ }
607
+ }
608
+ return null;
609
+ }
610
+
611
+ case 'prompt_injection': {
612
+ if (!event.content) return null;
613
+ const patterns = rule.conditions.patterns || [
614
+ 'ignore previous', 'ignore all previous', 'disregard your instructions',
615
+ 'you are now', 'new instructions:', 'system prompt:',
616
+ 'forget everything', 'override your',
617
+ ];
618
+ const contentLower = event.content.toLowerCase();
619
+ for (const pattern of patterns) {
620
+ if (contentLower.includes(pattern.toLowerCase())) {
621
+ return `Potential prompt injection detected: "${pattern}"`;
622
+ }
623
+ }
624
+ return null;
625
+ }
626
+
627
+ case 'data_leak_attempt': {
628
+ if (!event.content) return null;
629
+ const patterns = rule.conditions.patterns || [
630
+ '\\b\\d{3}-\\d{2}-\\d{4}\\b', // SSN
631
+ '\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b', // Credit card
632
+ '\\bpassword\\s*[:=]\\s*\\S+', // Password in text
633
+ ];
634
+ for (const pattern of patterns) {
635
+ try {
636
+ if (new RegExp(pattern, 'i').test(event.content)) {
637
+ return `Potential data leak: pattern "${pattern}" matched`;
638
+ }
639
+ } catch {} // invalid regex, skip
640
+ }
641
+ return null;
642
+ }
643
+
644
+ case 'off_hours': {
645
+ if (event.type !== 'session_start' && event.type !== 'tool_call') return null;
646
+ // Check against agent's schedule
647
+ try {
648
+ const schedRows = await this.engineDb.query<any>(
649
+ `SELECT * FROM work_schedules WHERE agent_id = $1 ORDER BY created_at DESC LIMIT 1`,
650
+ [event.agentId]
651
+ );
652
+ if (schedRows && schedRows.length > 0) {
653
+ const sched = schedRows[0];
654
+ const config = typeof sched.config === 'string' ? JSON.parse(sched.config) : (sched.config || {});
655
+ const hours = config.standardHours;
656
+ if (hours?.start && hours?.end) {
657
+ const tz = config.timezone || 'UTC';
658
+ const localTime = new Date(new Date().toLocaleString('en-US', { timeZone: tz }));
659
+ const currentHour = `${String(localTime.getHours()).padStart(2, '0')}:${String(localTime.getMinutes()).padStart(2, '0')}`;
660
+ if (currentHour < hours.start || currentHour >= hours.end) {
661
+ return `Activity outside work hours (${hours.start}-${hours.end} ${tz})`;
662
+ }
663
+ }
664
+ }
665
+ } catch {}
666
+ return null;
667
+ }
668
+
669
+ case 'memory_flood': {
670
+ if (event.type !== 'memory_write') return null;
671
+ const maxPerHour = rule.conditions.maxPerHour || 50;
672
+ try {
673
+ const since = new Date(Date.now() - 60 * 60_000).toISOString();
674
+ const rows = await this.engineDb.query<any>(
675
+ `SELECT COUNT(*) as cnt FROM agent_memory WHERE agent_id = $1 AND created_at >= $2`,
676
+ [event.agentId, since]
677
+ );
678
+ const count = rows?.[0]?.cnt || 0;
679
+ if (count > maxPerHour) {
680
+ return `Memory flood: ${count} writes in last hour (max: ${maxPerHour})`;
681
+ }
682
+ } catch {}
683
+ return null;
684
+ }
685
+
686
+ case 'tone_violation': {
687
+ if (!event.content || event.type !== 'email_send') return null;
688
+ const keywords = rule.conditions.keywords || ['urgent', 'asap', 'immediately'];
689
+ const contentLower = event.content.toLowerCase();
690
+ let violations = 0;
691
+ for (const kw of keywords) {
692
+ if (contentLower.includes(kw.toLowerCase())) violations++;
693
+ }
694
+ if (violations >= (rule.conditions.threshold || 2)) {
695
+ return `Tone issue: ${violations} flagged words in outgoing email`;
696
+ }
697
+ return null;
698
+ }
699
+
700
+ default:
701
+ return null;
702
+ }
703
+ }
704
+
705
+ private async recordTrigger(rule: any, event: any, detail: string): Promise<void> {
706
+ try {
707
+ // Update trigger count and last triggered
708
+ await this.engineDb.execute(
709
+ `UPDATE guardrail_rules SET trigger_count = trigger_count + 1, last_triggered_at = $1 WHERE id = $2`,
710
+ [new Date().toISOString(), rule.id]
711
+ );
712
+ // Update cached rule
713
+ rule.triggerCount = (rule.triggerCount || 0) + 1;
714
+ rule.lastTriggeredAt = new Date().toISOString();
715
+
716
+ // Record intervention
717
+ await this.engineDb.execute(
718
+ `INSERT INTO interventions (id, org_id, agent_id, type, reason, triggered_by, metadata, created_at) VALUES ($1, $2, $3, 'anomaly_detected', $4, 'guardrail_enforcer', $5, $6)`,
719
+ [crypto.randomUUID(), rule.orgId, event.agentId, `Rule "${rule.name}": ${detail}`,
720
+ JSON.stringify({ ruleId: rule.id, ruleType: rule.ruleType, eventType: event.type, severity: rule.severity }),
721
+ new Date().toISOString()]
722
+ );
723
+ } catch (err: any) {
724
+ console.warn(`[guardrail-enforcer] Failed to record trigger: ${err.message}`);
725
+ }
726
+ }
727
+ }