643 lines
17 KiB
TypeScript
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;
|
|
}
|