Three demo examples showcasing multi-agent orchestration: - **economics-seminar**: Hostile faculty panel debates AI economist presenter - **research-team**: Coordinator, Researcher, Analyst, Writer collaboration - **dungeon-master**: Persistent DM that creates its own game system 🤖 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta <noreply@letta.com>
334 lines
10 KiB
TypeScript
334 lines
10 KiB
TypeScript
/**
|
|
* Coordinator Agent
|
|
*
|
|
* Orchestrates the research workflow, manages quality control,
|
|
* and tracks team performance over time.
|
|
*/
|
|
|
|
import { createSession, resumeSession, type Session } from '../../../src/index.js';
|
|
import type { Depth, ResearchTask, UserFeedback } from '../types.js';
|
|
import { DEPTH_CONFIGS, generateTaskId, formatDuration } from '../types.js';
|
|
import { loadTeamState, saveTeamState, ARTIFACTS, readOutput, getOutputPath, outputExists } from '../tools/file-store.js';
|
|
import { createResearcher, runResearchTask, reflectOnTask as researcherReflect } from './researcher.js';
|
|
import { createAnalyst, runAnalysis, reflectOnTask as analystReflect } from './analyst.js';
|
|
import { createWriter, writeReport, reflectOnTask as writerReflect } from './writer.js';
|
|
|
|
const COORDINATOR_SYSTEM_PROMPT = `You are the Research Team Coordinator.
|
|
|
|
## Your Role
|
|
You orchestrate a research team of specialists:
|
|
- **Researcher**: Finds and evaluates academic sources
|
|
- **Analyst**: Synthesizes findings and identifies patterns
|
|
- **Writer**: Produces the final research report
|
|
|
|
You manage workflow, ensure quality, and learn from each task to improve team performance.
|
|
|
|
## Your Responsibilities
|
|
1. Receive research queries from users
|
|
2. Plan the research approach based on depth
|
|
3. Delegate tasks to team members
|
|
4. Review intermediate outputs for quality
|
|
5. Request iterations if quality is insufficient
|
|
6. Collect and distribute user feedback
|
|
7. Track team performance metrics
|
|
|
|
## Quality Thresholds
|
|
- Quick: Accept reasonable coverage of the topic
|
|
- Standard: Require thorough coverage with good synthesis
|
|
- Comprehensive: Require extensive coverage, deep analysis, polished writing
|
|
|
|
## Memory Usage
|
|
Your memory blocks:
|
|
- **team-performance**: Track metrics, success patterns, agent strengths
|
|
- **user-preferences**: Store feedback, preferred styles, past requests
|
|
- **research-history**: Brief summaries of completed research tasks
|
|
|
|
Use these to improve coordination over time.`;
|
|
|
|
/**
|
|
* Create or resume the coordinator agent
|
|
*/
|
|
export async function createCoordinator(
|
|
existingAgentId?: string | null
|
|
): Promise<Session> {
|
|
if (existingAgentId) {
|
|
return resumeSession(existingAgentId, {
|
|
model: 'haiku',
|
|
allowedTools: ['Glob', 'Read', 'Write'],
|
|
permissionMode: 'bypassPermissions',
|
|
});
|
|
}
|
|
|
|
return createSession({
|
|
model: 'haiku',
|
|
systemPrompt: COORDINATOR_SYSTEM_PROMPT,
|
|
memory: [
|
|
{
|
|
label: 'team-performance',
|
|
value: `# Team Performance Metrics
|
|
|
|
## Overall Stats
|
|
- Tasks Completed: 0
|
|
- Average Rating: N/A
|
|
- Success Rate: N/A
|
|
|
|
## Agent Performance
|
|
### Researcher
|
|
- Strengths: [To be discovered]
|
|
- Areas to Improve: [To be discovered]
|
|
|
|
### Analyst
|
|
- Strengths: [To be discovered]
|
|
- Areas to Improve: [To be discovered]
|
|
|
|
### Writer
|
|
- Strengths: [To be discovered]
|
|
- Areas to Improve: [To be discovered]
|
|
|
|
## Successful Patterns
|
|
[Record what works well]
|
|
|
|
## Lessons Learned
|
|
[Record mistakes and improvements]
|
|
`,
|
|
description: 'Track team metrics, success patterns, and agent strengths/weaknesses',
|
|
},
|
|
{
|
|
label: 'user-preferences',
|
|
value: `# User Preferences
|
|
|
|
## Feedback History
|
|
[Track user ratings and comments]
|
|
|
|
## Style Preferences
|
|
- Preferred depth: Unknown
|
|
- Citation style: Standard (Author, Year)
|
|
- Tone: Professional but accessible
|
|
|
|
## Common Requests
|
|
[Track recurring research topics or requirements]
|
|
`,
|
|
description: 'Store user feedback, preferences, and past requests',
|
|
},
|
|
{
|
|
label: 'research-history',
|
|
value: `# Research History
|
|
|
|
## Completed Tasks
|
|
[Brief summaries of past research]
|
|
|
|
## Topics Covered
|
|
[Track domains and topics researched]
|
|
|
|
## Notable Insights
|
|
[Key learnings that might help future research]
|
|
`,
|
|
description: 'Brief summaries of completed research tasks',
|
|
},
|
|
],
|
|
allowedTools: ['Glob', 'Read', 'Write'],
|
|
permissionMode: 'bypassPermissions',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Run a complete research workflow
|
|
*/
|
|
export async function runResearchWorkflow(
|
|
query: string,
|
|
depth: Depth,
|
|
onProgress?: (phase: string, message: string) => void
|
|
): Promise<{
|
|
success: boolean;
|
|
taskId: string;
|
|
reportPath: string;
|
|
durationMs: number;
|
|
}> {
|
|
const startTime = Date.now();
|
|
const taskId = generateTaskId();
|
|
const config = DEPTH_CONFIGS[depth];
|
|
|
|
const log = (phase: string, message: string) => {
|
|
onProgress?.(phase, message);
|
|
console.log(`[${phase}] ${message}`);
|
|
};
|
|
|
|
// Load team state
|
|
const teamState = await loadTeamState();
|
|
|
|
log('Init', `Starting research task: ${taskId}`);
|
|
log('Init', `Query: "${query}"`);
|
|
log('Init', `Depth: ${depth} (est. ${config.estimatedMinutes} min)`);
|
|
|
|
// Phase 1: Research
|
|
log('Research', 'Initializing researcher agent...');
|
|
const isNewResearcher = !teamState.agentIds.researcher;
|
|
const researcher = await createResearcher(teamState.agentIds.researcher, depth);
|
|
|
|
log('Research', `Searching for ${config.sourcesCount} sources...`);
|
|
const researchResult = await runResearchTask(researcher, taskId, query, depth);
|
|
|
|
// Save agent ID after first message exchange (when it becomes available)
|
|
if (isNewResearcher && researcher.agentId) {
|
|
teamState.agentIds.researcher = researcher.agentId;
|
|
await saveTeamState(teamState);
|
|
log('Research', `Created researcher agent: ${researcher.agentId}`);
|
|
} else if (researcher.agentId) {
|
|
log('Research', `Resumed researcher agent: ${researcher.agentId}`);
|
|
}
|
|
log('Research', ` → https://app.letta.com/agents/${researcher.agentId}`);
|
|
|
|
if (!researchResult.success) {
|
|
researcher.close();
|
|
return { success: false, taskId, reportPath: '', durationMs: Date.now() - startTime };
|
|
}
|
|
|
|
log('Research', `Found ${researchResult.sourcesFound} sources`);
|
|
log('Research', `Findings written to: ${researchResult.findingsPath}`);
|
|
researcher.close();
|
|
|
|
// Phase 2: Analysis
|
|
log('Analysis', 'Initializing analyst agent...');
|
|
const isNewAnalyst = !teamState.agentIds.analyst;
|
|
const analyst = await createAnalyst(teamState.agentIds.analyst, depth);
|
|
|
|
log('Analysis', 'Synthesizing findings...');
|
|
const analysisResult = await runAnalysis(analyst, taskId, query, depth);
|
|
|
|
// Save agent ID after first message exchange
|
|
if (isNewAnalyst && analyst.agentId) {
|
|
teamState.agentIds.analyst = analyst.agentId;
|
|
await saveTeamState(teamState);
|
|
log('Analysis', `Created analyst agent: ${analyst.agentId}`);
|
|
} else if (analyst.agentId) {
|
|
log('Analysis', `Resumed analyst agent: ${analyst.agentId}`);
|
|
}
|
|
log('Analysis', ` → https://app.letta.com/agents/${analyst.agentId}`);
|
|
|
|
if (!analysisResult.success) {
|
|
analyst.close();
|
|
return { success: false, taskId, reportPath: '', durationMs: Date.now() - startTime };
|
|
}
|
|
|
|
log('Analysis', `Analysis written to: ${analysisResult.analysisPath}`);
|
|
analyst.close();
|
|
|
|
// Phase 3: Writing
|
|
log('Writing', 'Initializing writer agent...');
|
|
const isNewWriter = !teamState.agentIds.writer;
|
|
const writer = await createWriter(teamState.agentIds.writer, depth);
|
|
|
|
log('Writing', 'Writing final report...');
|
|
const writeResult = await writeReport(writer, taskId, query, depth);
|
|
|
|
// Save agent ID after first message exchange
|
|
if (isNewWriter && writer.agentId) {
|
|
teamState.agentIds.writer = writer.agentId;
|
|
await saveTeamState(teamState);
|
|
log('Writing', `Created writer agent: ${writer.agentId}`);
|
|
} else if (writer.agentId) {
|
|
log('Writing', `Resumed writer agent: ${writer.agentId}`);
|
|
}
|
|
log('Writing', ` → https://app.letta.com/agents/${writer.agentId}`);
|
|
|
|
if (!writeResult.success) {
|
|
writer.close();
|
|
return { success: false, taskId, reportPath: '', durationMs: Date.now() - startTime };
|
|
}
|
|
|
|
log('Writing', `Report written to: ${writeResult.reportPath}`);
|
|
writer.close();
|
|
|
|
// Update team state
|
|
teamState.completedTasks++;
|
|
await saveTeamState(teamState);
|
|
|
|
const durationMs = Date.now() - startTime;
|
|
log('Complete', `Research complete in ${formatDuration(durationMs)}`);
|
|
|
|
return {
|
|
success: true,
|
|
taskId,
|
|
reportPath: writeResult.reportPath,
|
|
durationMs,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Process user feedback and trigger reflections
|
|
*/
|
|
export async function processFeedback(
|
|
taskId: string,
|
|
feedback: UserFeedback
|
|
): Promise<void> {
|
|
const teamState = await loadTeamState();
|
|
|
|
console.log('\n[Feedback] Processing feedback and triggering reflections...\n');
|
|
|
|
// Trigger reflections from each agent
|
|
if (teamState.agentIds.researcher) {
|
|
console.log('[Feedback] Researcher reflecting...');
|
|
const researcher = await createResearcher(teamState.agentIds.researcher);
|
|
const reflection = await researcherReflect(researcher, taskId, feedback);
|
|
console.log(`[Researcher] ${reflection.slice(0, 200)}...`);
|
|
researcher.close();
|
|
}
|
|
|
|
if (teamState.agentIds.analyst) {
|
|
console.log('[Feedback] Analyst reflecting...');
|
|
const analyst = await createAnalyst(teamState.agentIds.analyst);
|
|
const reflection = await analystReflect(analyst, taskId, feedback);
|
|
console.log(`[Analyst] ${reflection.slice(0, 200)}...`);
|
|
analyst.close();
|
|
}
|
|
|
|
if (teamState.agentIds.writer) {
|
|
console.log('[Feedback] Writer reflecting...');
|
|
const writer = await createWriter(teamState.agentIds.writer);
|
|
const reflection = await writerReflect(writer, taskId, feedback);
|
|
console.log(`[Writer] ${reflection.slice(0, 200)}...`);
|
|
writer.close();
|
|
}
|
|
|
|
console.log('\n[Feedback] All agents have reflected on the feedback.');
|
|
}
|
|
|
|
/**
|
|
* Get team status
|
|
*/
|
|
export async function getTeamStatus(): Promise<{
|
|
initialized: boolean;
|
|
agentIds: Record<string, string | null>;
|
|
completedTasks: number;
|
|
}> {
|
|
const state = await loadTeamState();
|
|
const initialized = Object.values(state.agentIds).some(id => id !== null);
|
|
|
|
return {
|
|
initialized,
|
|
agentIds: state.agentIds,
|
|
completedTasks: state.completedTasks,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset team (delete all agent IDs and start fresh)
|
|
*/
|
|
export async function resetTeam(): Promise<void> {
|
|
await saveTeamState({
|
|
agentIds: {
|
|
coordinator: null,
|
|
researcher: null,
|
|
analyst: null,
|
|
writer: null,
|
|
},
|
|
sharedBlockIds: {
|
|
sources: null,
|
|
terminology: null,
|
|
pitfalls: null,
|
|
},
|
|
completedTasks: 0,
|
|
});
|
|
console.log('[Reset] Team state cleared. New agents will be created on next run.');
|
|
}
|