@agenticmail/enterprise 0.5.167 → 0.5.169

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