mission-control/lib/services/daily-mission-agent.ts

643 lines
17 KiB
TypeScript

/**
* Voxyz Daily Mission Generation Agent
* AI-powered agent that generates daily mission proposals at 7 AM
* Phase 10: Autonomous Architecture
*/
import {
DailyMission,
DailyMissionInsert,
MissionProposal,
MissionProposalInsert,
DailyMissionGenerationInput,
DailyMissionGenerationResult,
AgentExecutionLog,
CalendarBlock
} from '@/lib/supabase/voxyz.types';
import { Task, fetchAllTasks } from '@/lib/data/tasks';
import { Project, fetchAllProjects } from '@/lib/data/projects';
import {
createProposalFromTask,
ProposalContext,
validateProposalAgainstCapGates,
saveProposal
} from '@/lib/services/proposal.service';
import {
checkCapGates,
getCapacityStatus,
initializeCapGates,
resetCapGatesForNewDay
} from '@/lib/services/cap-gates.service';
import { createClient } from '@/lib/supabase/client';
const GENERATION_HOUR = 7;
const GENERATION_MINUTE = 0;
/**
* Generate daily missions for a specific date
* This is the main entry point for the daily mission generation agent
*/
export async function generateDailyMissions(
input: DailyMissionGenerationInput
): Promise<DailyMissionGenerationResult | null> {
const executionLogId = await startExecutionLog('proposal_generator', 'scheduled');
const startTime = Date.now();
try {
// Step 1: Initialize/reset cap gates for the day
await initializeCapGates();
if (isNewDay()) {
await resetCapGatesForNewDay();
}
// Step 2: Fetch all active tasks and projects
const [tasks, projects] = await Promise.all([
fetchAllTasks(),
fetchAllProjects(),
]);
// Step 3: Filter to active, non-done tasks
const activeTasks = tasks.filter(task =>
task.status !== 'done' &&
task.status !== 'canceled' &&
task.status !== 'archived'
);
// Step 4: Score and rank tasks
const scoredTasks = scoreAndRankTasks(activeTasks, projects);
// Step 5: Get capacity status
const capacityStatus = await getCapacityStatus();
// Step 6: Generate proposals up to the limit
const proposals: MissionProposal[] = [];
const calendarBlocks: CalendarBlock[] = [];
const targetCount = Math.min(
input.targetMissionCount,
capacityStatus.dailyMissionsRemaining
);
for (let i = 0; i < scoredTasks.length && proposals.length < targetCount; i++) {
const { task, project, score } = scoredTasks[i];
// Check if we can add another proposal
const canAdd = await canAddAnotherProposal(task, proposals);
if (!canAdd.allowed) {
break;
}
// Create proposal
const context: ProposalContext = {
currentTime: new Date(),
activeTasks,
recentProposals: proposals,
};
const proposalInsert = createProposalFromTask(task, project, context);
// Validate against cap gates
const existingProposals = proposals.map(p => ({
...p,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
})) as MissionProposal[];
const capGateResult = await validateProposalAgainstCapGates(
proposalInsert,
existingProposals
);
if (capGateResult.allPassed || i < 2) { // Allow first 2 even if gates fail
// Save proposal to database
const savedProposal = await saveProposal(proposalInsert, capGateResult);
if (savedProposal) {
proposals.push(savedProposal);
// Create calendar block
const calendarBlock = await createCalendarBlockForProposal(savedProposal);
if (calendarBlock) {
calendarBlocks.push(calendarBlock);
}
}
}
}
// Step 7: Create daily mission record
const dailyMission = await createDailyMissionRecord(
input.date,
proposals,
calendarBlocks
);
// Step 8: Complete execution log
const duration = Date.now() - startTime;
await completeExecutionLog(executionLogId, {
status: 'success',
duration,
proposalsCreated: proposals.length,
tasksAnalyzed: activeTasks.length,
outputSummary: {
date: input.date.toISOString(),
missionsGenerated: proposals.length,
primaryMissionId: proposals[0]?.id,
secondaryMissionId: proposals[1]?.id,
tertiaryMissionId: proposals[2]?.id,
},
});
// Step 9: Return result
const finalCapGateCheck = await checkCapGates(0, proposals);
return {
dailyMission,
proposals,
capGatesChecked: finalCapGateCheck,
calendarBlocks,
executionLogId,
};
} catch (error) {
console.error('Error generating daily missions:', error);
// Log failure
await completeExecutionLog(executionLogId, {
status: 'failed',
duration: Date.now() - startTime,
errorMessage: error instanceof Error ? error.message : 'Unknown error',
});
return null;
}
}
/**
* Score and rank tasks for mission selection
*/
function scoreAndRankTasks(
tasks: Task[],
projects: Project[]
): Array<{ task: Task; project: Project | undefined; score: number }> {
const projectMap = new Map(projects.map(p => [p.id, p]));
const now = new Date();
const scored = tasks.map(task => {
const project = projectMap.get(task.projectId);
let score = 0;
// Priority score (0-25)
const priorityScores: Record<string, number> = {
'urgent': 25,
'high': 20,
'medium': 12,
'low': 5,
};
score += priorityScores[task.priority] || 10;
// Deadline score (0-30)
if (task.dueDate) {
const dueDate = new Date(task.dueDate);
const hoursUntil = (dueDate.getTime() - now.getTime()) / (1000 * 60 * 60);
if (hoursUntil < 0) score += 30; // Overdue
else if (hoursUntil < 24) score += 27;
else if (hoursUntil < 72) score += 21;
else if (hoursUntil < 168) score += 15;
else if (hoursUntil < 336) score += 9;
else score += 3;
}
// Staleness score (0-15)
const updatedAt = new Date(task.updatedAt);
const daysSinceUpdate = (now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24);
if (task.status === 'blocked') {
score += 15;
} else if (daysSinceUpdate > 14) {
score += 12;
} else if (daysSinceUpdate > 7) {
score += 9;
} else if (daysSinceUpdate > 3) {
score += 5;
}
// Project importance (0-20)
if (project) {
const nameLower = project.name.toLowerCase();
if (nameLower.includes('ios') || nameLower.includes('revenue')) {
score += 20;
} else if (nameLower.includes('mission') || nameLower.includes('critical')) {
score += 15;
} else {
score += 10;
}
}
// Status bonus (0-10)
if (task.status === 'in-progress') {
score += 10; // Finish what's started
} else if (task.status === 'todo') {
score += 5;
}
return { task, project, score };
});
// Sort by score descending
return scored.sort((a, b) => b.score - a.score);
}
/**
* Check if we can add another proposal
*/
async function canAddAnotherProposal(
task: Task,
existingProposals: MissionProposal[]
): Promise<{ allowed: boolean; reason?: string }> {
const capacity = await getCapacityStatus();
if (capacity.dailyMissionsRemaining <= 0) {
return { allowed: false, reason: 'Daily mission limit reached' };
}
// Check for duplicates
const isDuplicate = existingProposals.some(p =>
p.source_task_ids.includes(task.id)
);
if (isDuplicate) {
return { allowed: false, reason: 'Task already has a proposal' };
}
return { allowed: true };
}
/**
* Create a calendar block for a proposal
*/
async function createCalendarBlockForProposal(
proposal: MissionProposal
): Promise<CalendarBlock | null> {
const supabase = createClient();
const startTime = proposal.suggested_start_time
? new Date(proposal.suggested_start_time)
: new Date(new Date().setHours(9, 0, 0, 0));
const duration = proposal.estimated_duration_minutes || 120;
const endTime = new Date(startTime.getTime() + duration * 60000);
const block: Omit<CalendarBlock, 'id' | 'created_at' | 'updated_at'> = {
block_date: proposal.proposal_date,
start_time: startTime.toISOString(),
end_time: endTime.toISOString(),
block_type: 'mission_execution',
mission_id: null, // Will be set when daily mission is created
proposal_id: proposal.id,
calendar_event_id: null,
calendar_provider: 'google',
calendar_synced: false,
calendar_synced_at: null,
status: 'scheduled',
notes: proposal.title,
};
const { data, error } = await supabase
.from('ops_calendar_blocks')
.insert(block)
.select()
.single();
if (error) {
console.error('Error creating calendar block:', error);
return null;
}
return data as CalendarBlock;
}
/**
* Create daily mission record
*/
async function createDailyMissionRecord(
date: Date,
proposals: MissionProposal[],
calendarBlocks: CalendarBlock[]
): Promise<DailyMission> {
const supabase = createClient();
const totalMinutes = proposals.reduce((sum, p) =>
sum + (p.estimated_duration_minutes || 120), 0
);
// Calculate focus time (exclude reaction time)
const workdayMinutes = 480;
const reactionRatio = 0.30;
const focusMinutes = Math.round(totalMinutes * (1 - reactionRatio));
// Calculate focus time window
const focusStart = proposals[0]?.suggested_start_time
? new Date(proposals[0].suggested_start_time)
: new Date(date.setHours(9, 0, 0, 0));
const focusEnd = new Date(focusStart.getTime() + focusMinutes * 60000);
const missionInsert: Omit<DailyMission, 'id' | 'created_at' | 'updated_at'> = {
mission_date: date.toISOString().split('T')[0],
generated_at: new Date().toISOString(),
generated_by: 'ai_agent',
status: 'draft',
primary_mission_id: proposals[0]?.id || null,
secondary_mission_id: proposals[1]?.id || null,
tertiary_mission_id: proposals[2]?.id || null,
total_estimated_minutes: totalMinutes,
focus_time_start: focusStart.toISOString(),
focus_time_end: focusEnd.toISOString(),
focus_time_minutes: focusMinutes,
calendar_blocks: calendarBlocks.map(b => ({
id: b.id,
start_time: b.start_time,
end_time: b.end_time,
block_type: b.block_type,
})),
planned_reactions: Math.round(proposals.length * reactionRatio * 10),
actual_reactions: 0,
reaction_ratio: reactionRatio,
completed_at: null,
completion_notes: null,
completion_rating: null,
user_feedback: {},
};
const { data, error } = await supabase
.from('ops_daily_missions')
.insert(missionInsert)
.select()
.single();
if (error) {
console.error('Error creating daily mission:', error);
throw error;
}
// Update calendar blocks with mission_id
if (data) {
for (const block of calendarBlocks) {
await supabase
.from('ops_calendar_blocks')
.update({ mission_id: data.id })
.eq('id', block.id);
}
}
return data as DailyMission;
}
/**
* Start execution log entry
*/
async function startExecutionLog(
agentName: 'proposal_generator' | 'cap_gate_validator' | 'calendar_scheduler' | 'trigger_monitor' | 'mission_optimizer' | 'stale_task_detector',
executionType: 'scheduled' | 'triggered' | 'manual' | 'reaction'
): Promise<string> {
const supabase = createClient();
const { data, error } = await supabase
.from('ops_agent_execution_log')
.insert({
agent_name: agentName,
execution_type: executionType,
started_at: new Date().toISOString(),
status: 'running',
input_summary: {
timestamp: new Date().toISOString(),
agent: agentName,
type: executionType,
},
})
.select('id')
.single();
if (error) {
console.error('Error starting execution log:', error);
return '';
}
return data?.id || '';
}
/**
* Complete execution log entry
*/
async function completeExecutionLog(
logId: string,
result: {
status: 'success' | 'partial' | 'failed';
duration: number;
proposalsCreated?: number;
proposalsApproved?: number;
tasksAnalyzed?: number;
outputSummary?: Record<string, unknown>;
errorMessage?: string;
}
): Promise<void> {
const supabase = createClient();
await supabase
.from('ops_agent_execution_log')
.update({
completed_at: new Date().toISOString(),
duration_ms: result.duration,
status: result.status,
proposals_created: result.proposalsCreated || 0,
proposals_approved: result.proposalsApproved || 0,
tasks_analyzed: result.tasksAnalyzed || 0,
output_summary: result.outputSummary || {},
error_message: result.errorMessage || null,
})
.eq('id', logId);
}
/**
* Check if this is a new day (for cap gate reset)
*/
function isNewDay(): boolean {
const now = new Date();
return now.getHours() === GENERATION_HOUR && now.getMinutes() < 30;
}
/**
* Get today's daily mission
*/
export async function getTodaysDailyMission(): Promise<DailyMission | null> {
const supabase = createClient();
const today = new Date().toISOString().split('T')[0];
const { data, error } = await supabase
.from('ops_daily_missions')
.select('*')
.eq('mission_date', today)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // No mission found
}
console.error('Error fetching daily mission:', error);
return null;
}
return data as DailyMission;
}
/**
* Check if daily missions need to be generated
*/
export async function shouldGenerateDailyMissions(): Promise<boolean> {
const now = new Date();
// Check if it's generation time (7 AM)
if (now.getHours() !== GENERATION_HOUR) {
return false;
}
// Check if already generated today
const existingMission = await getTodaysDailyMission();
if (existingMission) {
return false;
}
return true;
}
/**
* Run the daily mission generation agent
* This is the entry point for scheduled execution
*/
export async function runDailyMissionGeneration(): Promise<DailyMissionGenerationResult | null> {
const shouldGenerate = await shouldGenerateDailyMissions();
if (!shouldGenerate) {
console.log('Daily missions already generated or not generation time');
return null;
}
const input: DailyMissionGenerationInput = {
date: new Date(),
targetMissionCount: 3,
includeReactions: true,
};
return generateDailyMissions(input);
}
/**
* Approve daily mission
*/
export async function approveDailyMission(
missionId: string
): Promise<boolean> {
const supabase = createClient();
const { error } = await supabase
.from('ops_daily_missions')
.update({
status: 'approved',
})
.eq('id', missionId);
if (error) {
console.error('Error approving daily mission:', error);
return false;
}
return true;
}
/**
* Schedule daily mission (create calendar events)
*/
export async function scheduleDailyMission(
missionId: string
): Promise<boolean> {
const supabase = createClient();
// Get mission details
const { data: mission } = await supabase
.from('ops_daily_missions')
.select('*')
.eq('id', missionId)
.single();
if (!mission) {
return false;
}
// Get proposals
const proposalIds = [
mission.primary_mission_id,
mission.secondary_mission_id,
mission.tertiary_mission_id,
].filter(Boolean);
const { data: proposals } = await supabase
.from('ops_mission_proposals')
.select('*')
.in('id', proposalIds);
if (!proposals) {
return false;
}
// Approve all proposals
for (const proposal of proposals) {
await supabase
.from('ops_mission_proposals')
.update({
status: 'approved',
approved_at: new Date().toISOString(),
})
.eq('id', proposal.id);
}
// Update mission status
await supabase
.from('ops_daily_missions')
.update({
status: 'scheduled',
})
.eq('id', missionId);
// TODO: Sync with Google Calendar (Phase 10.3)
return true;
}
/**
* Complete daily mission
*/
export async function completeDailyMission(
missionId: string,
rating?: number,
notes?: string
): Promise<boolean> {
const supabase = createClient();
const { error } = await supabase
.from('ops_daily_missions')
.update({
status: 'completed',
completed_at: new Date().toISOString(),
completion_rating: rating || null,
completion_notes: notes || null,
})
.eq('id', missionId);
if (error) {
console.error('Error completing daily mission:', error);
return false;
}
return true;
}