This commit is contained in:
126
packages/server/src/daemons/hive-mind.ts
Normal file
126
packages/server/src/daemons/hive-mind.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { eq, and, desc, gte, sql } from 'drizzle-orm';
|
||||
import { agentJobs, teamResources, tasks, messages, teamMembers } from '../db/schema.js';
|
||||
import type { Db } from '../db/connection.js';
|
||||
|
||||
export class HiveMindAgent {
|
||||
constructor(private db: Db) {}
|
||||
|
||||
async generateWeeklyDigest(teamId: string): Promise<{ digest: WeeklyDigest; messageId: string }> {
|
||||
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// Aggregate metrics
|
||||
const jobsCompleted = await this.db.select().from(agentJobs)
|
||||
.where(and(
|
||||
eq(agentJobs.teamId, teamId),
|
||||
eq(agentJobs.status, 'completed'),
|
||||
gte(agentJobs.completedAt, oneWeekAgo),
|
||||
));
|
||||
|
||||
const tasksCompleted = await this.db.select().from(tasks)
|
||||
.where(and(
|
||||
eq(tasks.teamId, teamId),
|
||||
eq(tasks.status, 'done'),
|
||||
));
|
||||
|
||||
const resourcesShared = await this.db.select().from(teamResources)
|
||||
.where(and(
|
||||
eq(teamResources.teamId, teamId),
|
||||
gte(teamResources.createdAt, oneWeekAgo),
|
||||
));
|
||||
|
||||
const waggleMessages = await this.db.select().from(messages)
|
||||
.where(and(
|
||||
eq(messages.teamId, teamId),
|
||||
gte(messages.createdAt, oneWeekAgo),
|
||||
));
|
||||
|
||||
// Detect duplicate work (similar task titles by different users)
|
||||
const duplicates = this.detectDuplicateWork(tasksCompleted);
|
||||
|
||||
// Find high-rated resources as best practices
|
||||
const bestPractices = await this.db.select().from(teamResources)
|
||||
.where(and(
|
||||
eq(teamResources.teamId, teamId),
|
||||
gte(teamResources.rating, sql`3.0`),
|
||||
))
|
||||
.orderBy(desc(teamResources.rating))
|
||||
.limit(5);
|
||||
|
||||
const digest: WeeklyDigest = {
|
||||
period: { from: oneWeekAgo, to: new Date() },
|
||||
metrics: {
|
||||
jobsCompleted: jobsCompleted.length,
|
||||
tasksCompleted: tasksCompleted.length,
|
||||
resourcesShared: resourcesShared.length,
|
||||
waggleMessages: waggleMessages.length,
|
||||
},
|
||||
duplicateWork: duplicates,
|
||||
bestPractices: bestPractices.map(r => ({ name: r.name, type: r.resourceType, rating: r.rating })),
|
||||
recommendations: this.generateRecommendations(jobsCompleted, duplicates, bestPractices),
|
||||
};
|
||||
|
||||
// Get a team member to attribute the broadcast to
|
||||
const [firstMember] = await this.db.select().from(teamMembers)
|
||||
.where(eq(teamMembers.teamId, teamId))
|
||||
.limit(1);
|
||||
|
||||
const senderId = firstMember?.userId ?? '';
|
||||
|
||||
// Broadcast digest as Waggle Dance message
|
||||
const [msg] = await this.db.insert(messages).values({
|
||||
teamId,
|
||||
senderId,
|
||||
type: 'broadcast',
|
||||
subtype: 'discovery',
|
||||
content: { type: 'weekly_digest', digest },
|
||||
}).returning();
|
||||
|
||||
return { digest, messageId: msg.id };
|
||||
}
|
||||
|
||||
private detectDuplicateWork(tasksList: Array<{ title: string; createdBy: string }>): DuplicateWork[] {
|
||||
// Group by similar titles (case-insensitive first 20 chars)
|
||||
const groups = new Map<string, { titles: string[]; users: Set<string> }>();
|
||||
for (const task of tasksList) {
|
||||
const key = task.title.toLowerCase().substring(0, 20);
|
||||
if (!groups.has(key)) groups.set(key, { titles: [], users: new Set() });
|
||||
groups.get(key)!.titles.push(task.title);
|
||||
groups.get(key)!.users.add(task.createdBy);
|
||||
}
|
||||
return Array.from(groups.values())
|
||||
.filter(g => g.users.size > 1)
|
||||
.map(g => ({ titles: g.titles, users: Array.from(g.users) }));
|
||||
}
|
||||
|
||||
private generateRecommendations(jobs: unknown[], duplicates: DuplicateWork[], bestPractices: unknown[]): string[] {
|
||||
const recommendations: string[] = [];
|
||||
if (duplicates.length > 0) {
|
||||
recommendations.push(`${duplicates.length} potential duplicate work detected. Consider checking the hive before starting tasks.`);
|
||||
}
|
||||
if (bestPractices.length > 0) {
|
||||
recommendations.push(`${bestPractices.length} highly-rated resources available. Share them team-wide.`);
|
||||
}
|
||||
if (jobs.length > 50) {
|
||||
recommendations.push('High job volume this week. Consider automating recurring tasks with cron schedules.');
|
||||
}
|
||||
return recommendations;
|
||||
}
|
||||
}
|
||||
|
||||
interface DuplicateWork {
|
||||
titles: string[];
|
||||
users: string[];
|
||||
}
|
||||
|
||||
interface WeeklyDigest {
|
||||
period: { from: Date; to: Date };
|
||||
metrics: {
|
||||
jobsCompleted: number;
|
||||
tasksCompleted: number;
|
||||
resourcesShared: number;
|
||||
waggleMessages: number;
|
||||
};
|
||||
duplicateWork: DuplicateWork[];
|
||||
bestPractices: Array<{ name: string; type: string; rating: number }>;
|
||||
recommendations: string[];
|
||||
}
|
||||
121
packages/server/src/daemons/scout.ts
Normal file
121
packages/server/src/daemons/scout.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { eq, and, desc } from 'drizzle-orm';
|
||||
import { scoutFindings, agents, teamMembers, teamResources } from '../db/schema.js';
|
||||
import type { Db } from '../db/connection.js';
|
||||
|
||||
interface Finding {
|
||||
source: string;
|
||||
category: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
relevanceScore: number;
|
||||
url: string | null;
|
||||
}
|
||||
|
||||
export class ScoutAgent {
|
||||
constructor(private db: Db) {}
|
||||
|
||||
async scan(userId: string, teamId: string): Promise<Array<typeof scoutFindings.$inferSelect>> {
|
||||
const findings: Finding[] = [];
|
||||
|
||||
// Source 1: Check team resources for newly shared items
|
||||
findings.push(...await this.checkTeamResources(teamId));
|
||||
|
||||
// Source 2: Mock marketplace check (would check npm/MCP registry in production)
|
||||
findings.push(...await this.checkMarketplace(userId));
|
||||
|
||||
// Score relevance based on user's agent configs and role
|
||||
const scored = await this.scoreRelevance(findings, userId, teamId);
|
||||
|
||||
// Filter out findings with titles already dismissed by this user
|
||||
const existingDismissed = await this.db.select().from(scoutFindings)
|
||||
.where(and(
|
||||
eq(scoutFindings.userId, userId),
|
||||
eq(scoutFindings.status, 'dismissed'),
|
||||
));
|
||||
const dismissedTitles = new Set(existingDismissed.map(f => f.title));
|
||||
const filtered = scored.filter(f => !dismissedTitles.has(f.title));
|
||||
|
||||
// Store findings
|
||||
const stored = [];
|
||||
for (const finding of filtered) {
|
||||
const [entry] = await this.db.insert(scoutFindings).values({
|
||||
userId,
|
||||
teamId,
|
||||
source: finding.source,
|
||||
category: finding.category,
|
||||
title: finding.title,
|
||||
summary: finding.summary,
|
||||
relevanceScore: finding.relevanceScore,
|
||||
url: finding.url,
|
||||
status: 'new',
|
||||
}).returning();
|
||||
stored.push(entry);
|
||||
}
|
||||
|
||||
return stored;
|
||||
}
|
||||
|
||||
private async checkTeamResources(teamId: string): Promise<Finding[]> {
|
||||
const recent = await this.db.select().from(teamResources)
|
||||
.where(eq(teamResources.teamId, teamId))
|
||||
.orderBy(desc(teamResources.createdAt))
|
||||
.limit(5);
|
||||
|
||||
return recent.map(r => ({
|
||||
source: 'team' as const,
|
||||
category: r.resourceType === 'skill' ? 'skill' : 'practice',
|
||||
title: `New team resource: ${r.name}`,
|
||||
summary: r.description ?? `A ${r.resourceType} shared by a team member`,
|
||||
relevanceScore: 0.5,
|
||||
url: null,
|
||||
}));
|
||||
}
|
||||
|
||||
private async checkMarketplace(_userId: string): Promise<Finding[]> {
|
||||
// Mock: In production, would check npm registry for MCP packages, skill marketplace, etc.
|
||||
return [];
|
||||
}
|
||||
|
||||
private async scoreRelevance(findings: Finding[], userId: string, teamId: string): Promise<Finding[]> {
|
||||
// Load user's agent configs for interest matching
|
||||
const userAgents = await this.db.select().from(agents)
|
||||
.where(eq(agents.userId, userId));
|
||||
|
||||
// Load member interests
|
||||
const [membership] = await this.db.select().from(teamMembers)
|
||||
.where(and(eq(teamMembers.teamId, teamId), eq(teamMembers.userId, userId)));
|
||||
|
||||
const interests = (membership?.interests as string[]) ?? [];
|
||||
const agentTools = userAgents.flatMap(a => (a.tools as string[]) ?? []);
|
||||
|
||||
return findings.map(f => {
|
||||
let score = f.relevanceScore;
|
||||
// Boost if matches user interests
|
||||
if (interests.some(i => f.title.toLowerCase().includes(i.toLowerCase()))) score += 0.3;
|
||||
// Boost if matches agent tool names
|
||||
if (agentTools.some(t => f.title.toLowerCase().includes(t.toLowerCase()))) score += 0.2;
|
||||
return { ...f, relevanceScore: Math.min(score, 1.0) };
|
||||
});
|
||||
}
|
||||
|
||||
async adopt(findingId: string) {
|
||||
const [updated] = await this.db.update(scoutFindings)
|
||||
.set({ status: 'adopted' })
|
||||
.where(eq(scoutFindings.id, findingId))
|
||||
.returning();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async dismiss(findingId: string) {
|
||||
const [updated] = await this.db.update(scoutFindings)
|
||||
.set({ status: 'dismissed' })
|
||||
.where(eq(scoutFindings.id, findingId))
|
||||
.returning();
|
||||
return updated ?? null;
|
||||
}
|
||||
|
||||
async listFindings(userId: string) {
|
||||
return this.db.select().from(scoutFindings)
|
||||
.where(eq(scoutFindings.userId, userId));
|
||||
}
|
||||
}
|
||||
94
packages/server/src/daemons/subconscious.ts
Normal file
94
packages/server/src/daemons/subconscious.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { eq, desc, and, sql } from 'drizzle-orm';
|
||||
import { agentJobs, agentAuditLog } from '../db/schema.js';
|
||||
import type { Db } from '../db/connection.js';
|
||||
import { SUBCONSCIOUS_INTERACTION_THRESHOLD } from '@waggle/shared';
|
||||
|
||||
export class SubconsciousAgent {
|
||||
constructor(private db: Db) {}
|
||||
|
||||
async shouldReflect(userId: string): Promise<boolean> {
|
||||
// Count completed jobs since last reflection
|
||||
const lastReflection = await this.db.select().from(agentAuditLog)
|
||||
.where(and(
|
||||
eq(agentAuditLog.userId, userId),
|
||||
eq(agentAuditLog.actionType, 'subconscious_reflection'),
|
||||
))
|
||||
.orderBy(desc(agentAuditLog.createdAt))
|
||||
.limit(1);
|
||||
|
||||
const since = lastReflection[0]?.createdAt ?? new Date(0);
|
||||
const sinceIso = since.toISOString();
|
||||
|
||||
const recentJobs = await this.db.select().from(agentJobs)
|
||||
.where(and(
|
||||
eq(agentJobs.userId, userId),
|
||||
eq(agentJobs.status, 'completed'),
|
||||
sql`${agentJobs.completedAt} > ${sinceIso}::timestamptz`,
|
||||
));
|
||||
|
||||
return recentJobs.length >= SUBCONSCIOUS_INTERACTION_THRESHOLD;
|
||||
}
|
||||
|
||||
async reflect(userId: string): Promise<{ auditEntry: typeof agentAuditLog.$inferSelect; insights: Insight[] }> {
|
||||
// Get recent completed jobs
|
||||
const recentJobs = await this.db.select().from(agentJobs)
|
||||
.where(and(
|
||||
eq(agentJobs.userId, userId),
|
||||
eq(agentJobs.status, 'completed'),
|
||||
))
|
||||
.orderBy(desc(agentJobs.completedAt))
|
||||
.limit(20);
|
||||
|
||||
// Analyze patterns
|
||||
const insights = this.analyzePatterns(recentJobs);
|
||||
|
||||
// Log the reflection
|
||||
const [auditEntry] = await this.db.insert(agentAuditLog).values({
|
||||
userId,
|
||||
agentName: 'subconscious',
|
||||
actionType: 'subconscious_reflection',
|
||||
description: `Reflected on ${recentJobs.length} recent jobs. Found ${insights.length} insights.`,
|
||||
afterState: { insights },
|
||||
requiresApproval: insights.some(i => i.type === 'prompt_change'),
|
||||
}).returning();
|
||||
|
||||
return { auditEntry, insights };
|
||||
}
|
||||
|
||||
private analyzePatterns(jobs: Array<typeof agentJobs.$inferSelect>): Insight[] {
|
||||
const insights: Insight[] = [];
|
||||
|
||||
// Pattern: repeated job types
|
||||
const typeCounts = new Map<string, number>();
|
||||
for (const job of jobs) {
|
||||
typeCounts.set(job.jobType, (typeCounts.get(job.jobType) ?? 0) + 1);
|
||||
}
|
||||
for (const [type, count] of typeCounts) {
|
||||
if (count >= 5) {
|
||||
insights.push({
|
||||
type: 'prompt_change',
|
||||
description: `Job type "${type}" executed ${count} times recently`,
|
||||
recommendation: `Consider optimizing the system prompt for ${type} tasks`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern: failed jobs
|
||||
const failedCount = jobs.filter(j => j.status === 'failed').length;
|
||||
if (failedCount >= 3) {
|
||||
insights.push({
|
||||
type: 'tool_issue',
|
||||
description: `${failedCount} jobs failed recently`,
|
||||
recommendation: 'Review tool configurations and consider adding error handling',
|
||||
});
|
||||
}
|
||||
|
||||
return insights;
|
||||
}
|
||||
}
|
||||
|
||||
interface Insight {
|
||||
type: string;
|
||||
description: string;
|
||||
recommendation: string;
|
||||
}
|
||||
Reference in New Issue
Block a user