feat: add multi-agent demo examples
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>
This commit is contained in:
11
examples/.gitignore
vendored
Normal file
11
examples/.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Generated state files
|
||||
**/state.json
|
||||
**/seminar-state.json
|
||||
**/team-state.json
|
||||
|
||||
# Generated output
|
||||
**/output/
|
||||
**/transcripts/
|
||||
|
||||
# Generated rulebooks (created by DM)
|
||||
**/rulebook.md
|
||||
215
examples/dungeon-master/cli.ts
Executable file
215
examples/dungeon-master/cli.ts
Executable file
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Dungeon Master CLI
|
||||
*
|
||||
* A persistent DM that creates its own game system and remembers your campaigns.
|
||||
*
|
||||
* Usage:
|
||||
* bun cli.ts # Continue current campaign or start new
|
||||
* bun cli.ts --new # Start a new campaign
|
||||
* bun cli.ts --campaign=NAME # Switch to a specific campaign
|
||||
* bun cli.ts --list # List all campaigns
|
||||
* bun cli.ts --status # Show DM and campaign status
|
||||
* bun cli.ts --rulebook # Display the DM's rulebook
|
||||
* bun cli.ts --reset # Delete everything and start fresh
|
||||
*/
|
||||
|
||||
import { parseArgs } from 'node:util';
|
||||
import * as readline from 'node:readline';
|
||||
import {
|
||||
loadState,
|
||||
saveState,
|
||||
createDM,
|
||||
initializeDM,
|
||||
startNewCampaign,
|
||||
resumeCampaign,
|
||||
playSession,
|
||||
showStatus,
|
||||
resetAll,
|
||||
hasRulebook,
|
||||
readRulebook,
|
||||
listCampaigns,
|
||||
} from './dm.js';
|
||||
|
||||
/**
|
||||
* Prompt user for input
|
||||
*/
|
||||
function prompt(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
new: { type: 'boolean', default: false },
|
||||
campaign: { type: 'string' },
|
||||
list: { type: 'boolean', default: false },
|
||||
status: { type: 'boolean', default: false },
|
||||
rulebook: { type: 'boolean', default: false },
|
||||
reset: { type: 'boolean', default: false },
|
||||
help: { type: 'boolean', short: 'h', default: false },
|
||||
},
|
||||
});
|
||||
|
||||
if (values.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.reset) {
|
||||
const confirm = await prompt('⚠️ Delete DM and all campaigns? (yes/no): ');
|
||||
if (confirm.toLowerCase() === 'yes') {
|
||||
await resetAll();
|
||||
} else {
|
||||
console.log('Cancelled.\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.status) {
|
||||
const state = await loadState();
|
||||
await showStatus(state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.list) {
|
||||
const campaigns = await listCampaigns();
|
||||
console.log('\n📜 Campaigns:\n');
|
||||
if (campaigns.length === 0) {
|
||||
console.log(' (no campaigns yet)\n');
|
||||
} else {
|
||||
for (const name of campaigns) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.rulebook) {
|
||||
const rulebook = await readRulebook();
|
||||
if (rulebook) {
|
||||
console.log('\n📖 DM\'s Rulebook:\n');
|
||||
console.log(rulebook);
|
||||
} else {
|
||||
console.log('\n📖 No rulebook yet. Start a session to have the DM create one.\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Main game flow
|
||||
const state = await loadState();
|
||||
|
||||
// Create or resume DM
|
||||
const dm = await createDM(state);
|
||||
|
||||
// Save DM ID if new
|
||||
if (!state.dmAgentId && dm.agentId) {
|
||||
state.dmAgentId = dm.agentId;
|
||||
await saveState(state);
|
||||
}
|
||||
|
||||
// If no rulebook, initialize the DM first
|
||||
if (!hasRulebook()) {
|
||||
await initializeDM(dm, state);
|
||||
}
|
||||
|
||||
// Determine what to do
|
||||
if (values.new || values.campaign) {
|
||||
// Starting or switching campaigns
|
||||
const campaignName = values.campaign || await prompt('📜 Campaign name: ');
|
||||
if (!campaignName) {
|
||||
console.log('No campaign name provided.\n');
|
||||
dm.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const campaigns = await listCampaigns();
|
||||
if (campaigns.includes(campaignName)) {
|
||||
// Resume existing
|
||||
await resumeCampaign(dm, state, campaignName);
|
||||
} else {
|
||||
// Start new
|
||||
await startNewCampaign(dm, state, campaignName);
|
||||
}
|
||||
} else if (state.activeCampaign) {
|
||||
// Resume current campaign
|
||||
await resumeCampaign(dm, state, state.activeCampaign);
|
||||
} else {
|
||||
// No active campaign, start new
|
||||
console.log('\n🎲 Welcome to Dungeon Master!\n');
|
||||
console.log('No active campaign. Let\'s start one.\n');
|
||||
|
||||
const campaignName = await prompt('📜 Campaign name: ');
|
||||
if (!campaignName) {
|
||||
console.log('No campaign name provided.\n');
|
||||
dm.close();
|
||||
return;
|
||||
}
|
||||
|
||||
await startNewCampaign(dm, state, campaignName);
|
||||
}
|
||||
|
||||
// Enter gameplay loop
|
||||
await playSession(dm);
|
||||
|
||||
dm.close();
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
🎲 Dungeon Master
|
||||
|
||||
A persistent DM that creates its own game system and remembers your campaigns.
|
||||
|
||||
USAGE:
|
||||
bun cli.ts [options]
|
||||
|
||||
OPTIONS:
|
||||
--new Start a new campaign
|
||||
--campaign=NAME Switch to or create a specific campaign
|
||||
--list List all campaigns
|
||||
--status Show DM and campaign status
|
||||
--rulebook Display the DM's custom rulebook
|
||||
--reset Delete everything and start fresh
|
||||
-h, --help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
bun cli.ts # Continue current campaign
|
||||
bun cli.ts --new # Start a new campaign
|
||||
bun cli.ts --campaign=dragons # Play the "dragons" campaign
|
||||
bun cli.ts --rulebook # See the DM's game system
|
||||
|
||||
FEATURES:
|
||||
📖 The DM writes its own rulebook - a custom game system it creates and refines
|
||||
🗺️ Persistent campaigns - come back days later and pick up where you left off
|
||||
👥 NPCs remember you - "You're the one who spared the goblin!"
|
||||
⚡ Consequences matter - past actions shape future events
|
||||
🎭 Collaborative tone - the DM adapts to what you enjoy
|
||||
|
||||
GAMEPLAY:
|
||||
Type your actions, dialogue, or questions. The DM will respond.
|
||||
|
||||
Special commands:
|
||||
save - Save current progress
|
||||
quit - End session (auto-saves)
|
||||
|
||||
Each campaign is stored in campaigns/{name}/ with files for world, character,
|
||||
NPCs, quests, and session history. The DM reads and writes these to maintain
|
||||
perfect memory across sessions.
|
||||
`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
414
examples/dungeon-master/dm.ts
Normal file
414
examples/dungeon-master/dm.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* Dungeon Master Agent
|
||||
*
|
||||
* A persistent DM that creates its own game system and runs campaigns.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../src/index.js';
|
||||
import { GameState, DEFAULT_CONFIG, PATHS, CAMPAIGN_FILES } from './types.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const STATE_FILE = join(__dirname, PATHS.stateFile);
|
||||
const RULEBOOK_FILE = join(__dirname, PATHS.rulebook);
|
||||
const CAMPAIGNS_DIR = join(__dirname, PATHS.campaignsDir);
|
||||
|
||||
// ANSI colors
|
||||
const COLORS = {
|
||||
dm: '\x1b[35m', // Magenta for DM
|
||||
player: '\x1b[36m', // Cyan for player
|
||||
system: '\x1b[90m', // Gray for system messages
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Load game state from disk
|
||||
*/
|
||||
export async function loadState(): Promise<GameState> {
|
||||
if (existsSync(STATE_FILE)) {
|
||||
const data = await readFile(STATE_FILE, 'utf-8');
|
||||
return JSON.parse(data);
|
||||
}
|
||||
return {
|
||||
dmAgentId: null,
|
||||
activeCampaign: null,
|
||||
campaigns: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save game state to disk
|
||||
*/
|
||||
export async function saveState(state: GameState): Promise<void> {
|
||||
await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get campaign directory path
|
||||
*/
|
||||
function getCampaignDir(campaignName: string): string {
|
||||
return join(CAMPAIGNS_DIR, campaignName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get campaign file path
|
||||
*/
|
||||
function getCampaignFile(campaignName: string, file: keyof typeof CAMPAIGN_FILES): string {
|
||||
return join(getCampaignDir(campaignName), CAMPAIGN_FILES[file]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rulebook exists
|
||||
*/
|
||||
export function hasRulebook(): boolean {
|
||||
return existsSync(RULEBOOK_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the rulebook
|
||||
*/
|
||||
export async function readRulebook(): Promise<string | null> {
|
||||
if (!hasRulebook()) return null;
|
||||
return readFile(RULEBOOK_FILE, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* List all campaigns
|
||||
*/
|
||||
export async function listCampaigns(): Promise<string[]> {
|
||||
if (!existsSync(CAMPAIGNS_DIR)) return [];
|
||||
const entries = await readdir(CAMPAIGNS_DIR, { withFileTypes: true });
|
||||
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or resume the DM agent
|
||||
*/
|
||||
export async function createDM(state: GameState): Promise<Session> {
|
||||
if (state.dmAgentId) {
|
||||
// Resume existing DM
|
||||
return resumeSession(state.dmAgentId, {
|
||||
model: DEFAULT_CONFIG.model,
|
||||
allowedTools: ['Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
// Create new DM
|
||||
const session = await createSession({
|
||||
model: DEFAULT_CONFIG.model,
|
||||
systemPrompt: `You are a Dungeon Master - a creative storyteller and game designer who runs tabletop RPG campaigns.
|
||||
|
||||
## Your Role
|
||||
- Design and run engaging tabletop RPG experiences
|
||||
- Create your own game system with rules you write in rulebook.md
|
||||
- Manage persistent campaign worlds that remember everything
|
||||
- Collaborate with players on tone, style, and what kind of experience they want
|
||||
|
||||
## Your Style
|
||||
- Adapt to what the player wants (serious, funny, dark, lighthearted)
|
||||
- Be descriptive and immersive in narration
|
||||
- Give players meaningful choices with real consequences
|
||||
- Keep things moving - don't get bogged down in rules
|
||||
|
||||
## File Management
|
||||
You have access to Read and Write tools. Use them to:
|
||||
- Write and update your rulebook (rulebook.md)
|
||||
- Manage campaign files in campaigns/{name}/:
|
||||
- world.md - Setting, locations, lore
|
||||
- player.md - Character sheet, backstory, inventory
|
||||
- npcs.md - NPCs met, relationships
|
||||
- quests.md - Active/completed quests
|
||||
- session-log.md - What happened each session
|
||||
- consequences.md - Pending events from past actions
|
||||
|
||||
## Important
|
||||
- Always update campaign files after significant events
|
||||
- Reference your rulebook when resolving actions
|
||||
- Make the world feel alive and reactive to player choices`,
|
||||
memory: [
|
||||
{
|
||||
label: 'campaign-state',
|
||||
value: `# Current Campaign State
|
||||
|
||||
## Active Campaign
|
||||
None - waiting to start or load a campaign
|
||||
|
||||
## Recent Events
|
||||
(none yet)
|
||||
|
||||
## Pending Consequences
|
||||
(none yet)`,
|
||||
description: 'Track the current campaign state and important recent events',
|
||||
},
|
||||
{
|
||||
label: 'player-preferences',
|
||||
value: `# Player Preferences
|
||||
|
||||
## Tone
|
||||
(not yet established - ask the player)
|
||||
|
||||
## Play Style
|
||||
(not yet established)
|
||||
|
||||
## Boundaries
|
||||
(ask if there are topics to avoid)`,
|
||||
description: 'Remember what the player enjoys and any boundaries',
|
||||
},
|
||||
],
|
||||
allowedTools: ['Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream output with color
|
||||
*/
|
||||
function createStreamPrinter(): (text: string) => void {
|
||||
return (text: string) => {
|
||||
process.stdout.write(`${COLORS.dm}${text}${COLORS.reset}`);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the DM and get response
|
||||
*/
|
||||
export async function chat(
|
||||
session: Session,
|
||||
message: string,
|
||||
onOutput?: (text: string) => void
|
||||
): Promise<string> {
|
||||
await session.send(message);
|
||||
|
||||
let response = '';
|
||||
const printer = onOutput || createStreamPrinter();
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
printer(msg.content);
|
||||
} else if (msg.type === 'tool_call') {
|
||||
console.log(`\n${COLORS.system}[Using ${msg.name}...]${COLORS.reset}`);
|
||||
} else if (msg.type === 'tool_result') {
|
||||
console.log(`${COLORS.system}[Done]${COLORS.reset}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a new DM (create rulebook)
|
||||
*/
|
||||
export async function initializeDM(session: Session, state: GameState): Promise<void> {
|
||||
console.log(`\n${COLORS.system}The DM is creating its game system...${COLORS.reset}\n`);
|
||||
|
||||
const prompt = `You're starting fresh as a Dungeon Master. Your first task is to create your game system.
|
||||
|
||||
Write a rulebook.md file that contains your custom tabletop RPG rules. Include:
|
||||
|
||||
1. **Core Mechanic** - How do players resolve actions? (dice, cards, narrative, etc.)
|
||||
2. **Character Stats** - What defines a character mechanically?
|
||||
3. **Combat** - How does fighting work?
|
||||
4. **Skills/Abilities** - What can characters do?
|
||||
5. **Progression** - How do characters grow?
|
||||
6. **Health/Death** - How does damage and dying work?
|
||||
|
||||
Keep it simple but complete enough to run a game. You can always refine it later.
|
||||
|
||||
Use the Write tool to create rulebook.md now.`;
|
||||
|
||||
await chat(session, prompt, createStreamPrinter());
|
||||
|
||||
// Save the DM agent ID
|
||||
if (session.agentId) {
|
||||
state.dmAgentId = session.agentId;
|
||||
await saveState(state);
|
||||
}
|
||||
|
||||
console.log(`\n\n${COLORS.system}Rulebook created! The DM is ready.${COLORS.reset}`);
|
||||
console.log(`${COLORS.system}[DM Agent: ${session.agentId}]${COLORS.reset}`);
|
||||
console.log(`${COLORS.system}[→ https://app.letta.com/agents/${session.agentId}]${COLORS.reset}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new campaign
|
||||
*/
|
||||
export async function startNewCampaign(
|
||||
session: Session,
|
||||
state: GameState,
|
||||
campaignName: string
|
||||
): Promise<void> {
|
||||
// Create campaign directory
|
||||
const campaignDir = getCampaignDir(campaignName);
|
||||
await mkdir(campaignDir, { recursive: true });
|
||||
|
||||
// Update state
|
||||
state.activeCampaign = campaignName;
|
||||
if (!state.campaigns.includes(campaignName)) {
|
||||
state.campaigns.push(campaignName);
|
||||
}
|
||||
await saveState(state);
|
||||
|
||||
console.log(`\n${COLORS.system}Starting new campaign: ${campaignName}${COLORS.reset}\n`);
|
||||
|
||||
const prompt = `We're starting a new campaign called "${campaignName}".
|
||||
|
||||
The campaign files are in: campaigns/${campaignName}/
|
||||
|
||||
First, let's create this world together. Ask me:
|
||||
1. What kind of setting/world interests me?
|
||||
2. What tone am I looking for? (serious, comedic, dark, heroic, etc.)
|
||||
3. Any topics I'd like to avoid?
|
||||
|
||||
Then ask about my character concept - I'll describe who I want to play and you'll help fill in the mechanical details based on your rulebook.
|
||||
|
||||
Start by greeting me and asking these questions.`;
|
||||
|
||||
await chat(session, prompt, createStreamPrinter());
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume an existing campaign
|
||||
*/
|
||||
export async function resumeCampaign(
|
||||
session: Session,
|
||||
state: GameState,
|
||||
campaignName: string
|
||||
): Promise<void> {
|
||||
state.activeCampaign = campaignName;
|
||||
await saveState(state);
|
||||
|
||||
console.log(`\n${COLORS.system}Resuming campaign: ${campaignName}${COLORS.reset}\n`);
|
||||
|
||||
const prompt = `We're resuming the campaign "${campaignName}".
|
||||
|
||||
Please:
|
||||
1. Read the campaign files in campaigns/${campaignName}/ to refresh your memory
|
||||
2. Read your rulebook.md if needed
|
||||
3. Give me a brief recap of where we left off
|
||||
4. Set the scene for our next moment of play
|
||||
|
||||
Use the Read tool to load the campaign state, then continue our adventure.`;
|
||||
|
||||
await chat(session, prompt, createStreamPrinter());
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main gameplay loop
|
||||
*/
|
||||
export async function playSession(session: Session): Promise<void> {
|
||||
const readline = await import('node:readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const askQuestion = (prompt: string): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(prompt, (answer) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
console.log(`${COLORS.system}(Type 'quit' to end session, 'save' to save progress)${COLORS.reset}\n`);
|
||||
|
||||
while (true) {
|
||||
const input = await askQuestion(`${COLORS.player}> ${COLORS.reset}`);
|
||||
|
||||
if (input.toLowerCase() === 'quit' || input.toLowerCase() === 'exit') {
|
||||
// Ask DM to save state
|
||||
console.log(`\n${COLORS.system}Ending session...${COLORS.reset}\n`);
|
||||
await chat(session, `The player is ending the session. Please:
|
||||
1. Update the session-log.md with a summary of what happened this session
|
||||
2. Update any other campaign files that need changes (player.md, npcs.md, quests.md, consequences.md)
|
||||
3. Give a brief farewell that hints at what might come next
|
||||
|
||||
Use the Write tool to save everything.`, createStreamPrinter());
|
||||
console.log('\n');
|
||||
break;
|
||||
}
|
||||
|
||||
if (input.toLowerCase() === 'save') {
|
||||
console.log(`\n${COLORS.system}Saving progress...${COLORS.reset}\n`);
|
||||
await chat(session, `Please save the current game state:
|
||||
1. Update session-log.md with recent events
|
||||
2. Update player.md with any changes to the character
|
||||
3. Update any other files that need it
|
||||
|
||||
Use the Write tool to save.`, createStreamPrinter());
|
||||
console.log('\n');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!input.trim()) continue;
|
||||
|
||||
// Send player input to DM
|
||||
console.log('');
|
||||
await chat(session, input, createStreamPrinter());
|
||||
console.log('\n');
|
||||
}
|
||||
|
||||
rl.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show game status
|
||||
*/
|
||||
export async function showStatus(state: GameState): Promise<void> {
|
||||
console.log('\n🎲 Dungeon Master Status\n');
|
||||
|
||||
console.log(`DM Agent: ${state.dmAgentId || '(not created)'}`);
|
||||
if (state.dmAgentId) {
|
||||
console.log(` → https://app.letta.com/agents/${state.dmAgentId}`);
|
||||
}
|
||||
|
||||
console.log(`\nRulebook: ${hasRulebook() ? '✓ Created' : '✗ Not created'}`);
|
||||
|
||||
console.log(`\nActive Campaign: ${state.activeCampaign || '(none)'}`);
|
||||
|
||||
const campaigns = await listCampaigns();
|
||||
console.log(`\nCampaigns (${campaigns.length}):`);
|
||||
if (campaigns.length === 0) {
|
||||
console.log(' (no campaigns yet)');
|
||||
} else {
|
||||
for (const name of campaigns) {
|
||||
const marker = name === state.activeCampaign ? ' ← active' : '';
|
||||
console.log(` - ${name}${marker}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset everything
|
||||
*/
|
||||
export async function resetAll(): Promise<void> {
|
||||
const fs = await import('node:fs/promises');
|
||||
|
||||
if (existsSync(STATE_FILE)) {
|
||||
await fs.unlink(STATE_FILE);
|
||||
}
|
||||
if (existsSync(RULEBOOK_FILE)) {
|
||||
await fs.unlink(RULEBOOK_FILE);
|
||||
}
|
||||
if (existsSync(CAMPAIGNS_DIR)) {
|
||||
await fs.rm(CAMPAIGNS_DIR, { recursive: true });
|
||||
await mkdir(CAMPAIGNS_DIR);
|
||||
}
|
||||
|
||||
console.log('\n🗑️ Reset complete. DM and all campaigns deleted.\n');
|
||||
}
|
||||
39
examples/dungeon-master/types.ts
Normal file
39
examples/dungeon-master/types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Dungeon Master Types
|
||||
*/
|
||||
|
||||
export interface Campaign {
|
||||
name: string;
|
||||
createdAt: Date;
|
||||
lastPlayed: Date;
|
||||
sessionCount: number;
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
dmAgentId: string | null;
|
||||
activeCampaign: string | null;
|
||||
campaigns: string[];
|
||||
}
|
||||
|
||||
export interface DMConfig {
|
||||
model: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: DMConfig = {
|
||||
model: 'haiku',
|
||||
};
|
||||
|
||||
export const PATHS = {
|
||||
stateFile: 'state.json',
|
||||
rulebook: 'rulebook.md',
|
||||
campaignsDir: 'campaigns',
|
||||
} as const;
|
||||
|
||||
export const CAMPAIGN_FILES = {
|
||||
world: 'world.md',
|
||||
player: 'player.md',
|
||||
npcs: 'npcs.md',
|
||||
quests: 'quests.md',
|
||||
sessionLog: 'session-log.md',
|
||||
consequences: 'consequences.md',
|
||||
} as const;
|
||||
182
examples/economics-seminar/README.md
Normal file
182
examples/economics-seminar/README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Economics Seminar
|
||||
|
||||
A multi-agent academic seminar simulation built on the Letta Code SDK.
|
||||
|
||||
An economist agent researches and presents findings, then defends their work against a faculty panel of specialists. Each agent has persistent memory and learns from each seminar.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
cd examples/economics-seminar
|
||||
bun cli.ts
|
||||
```
|
||||
|
||||
## What Happens
|
||||
|
||||
1. **📚 Research Phase**: The presenter agent picks a topic and uses `web_search` to research it
|
||||
2. **📖 Presentation**: The presenter delivers their findings
|
||||
3. **❓ Q&A Session**: Each faculty member asks questions, with back-and-forth follow-ups
|
||||
4. **💭 Reflection**: Faculty members share final thoughts and update their memories
|
||||
|
||||
## The Cast
|
||||
|
||||
### Presenter (Economist)
|
||||
- Picks compelling research topics
|
||||
- Uses web search to find papers, data, evidence
|
||||
- Presents findings and defends methodology
|
||||
- Learns from faculty feedback over time
|
||||
|
||||
### Faculty Panel
|
||||
|
||||
| Role | Name | Perspective |
|
||||
|------|------|-------------|
|
||||
| **Macro** | Dr. Chen | Policy implications, aggregate effects, systemic impacts |
|
||||
| **Micro** | Dr. Roberts | Incentives, equilibrium, theoretical rigor |
|
||||
| **Behavioral** | Dr. Patel | Psychology, biases, how real humans behave |
|
||||
| **Historian** | Dr. Morrison | Historical precedent, what's been tried before |
|
||||
|
||||
## Configuration
|
||||
|
||||
```bash
|
||||
# Quick seminar (3 faculty, 1 question each)
|
||||
bun cli.ts --faculty=3 --rounds=1
|
||||
|
||||
# Full panel, longer discussion
|
||||
bun cli.ts --faculty=4 --rounds=3
|
||||
|
||||
# Check agent status
|
||||
bun cli.ts --status
|
||||
|
||||
# Reset all agents
|
||||
bun cli.ts --reset
|
||||
```
|
||||
|
||||
## Live Transcript
|
||||
|
||||
The seminar streams a colored transcript as it runs:
|
||||
|
||||
```
|
||||
═══════════════════════════════════════════════════════════════
|
||||
🎓 ECONOMICS SEMINAR
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
Seminar #1
|
||||
Faculty panel: 3 members
|
||||
Q&A rounds: up to 2 per faculty member
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
📖 RESEARCH & PRESENTATION
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
**Presenter** is preparing...
|
||||
|
||||
I'll research the topic of automation and labor market impacts...
|
||||
[uses web_search]
|
||||
...
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
❓ Q&A SESSION
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
─── Dr. Chen (Professor of Macroeconomics) ───
|
||||
|
||||
**Dr. Chen**:
|
||||
Your analysis focuses on individual job displacement, but what about the
|
||||
aggregate demand effects? If automation reduces wages broadly, who buys
|
||||
the products these automated systems produce?
|
||||
|
||||
**Presenter**:
|
||||
That's an excellent point about the demand-side effects...
|
||||
|
||||
**Dr. Chen** (follow-up):
|
||||
But doesn't your model assume...
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
## Agent Persistence
|
||||
|
||||
Each agent maintains memory blocks that persist across seminars:
|
||||
|
||||
**Presenter memories:**
|
||||
- `research-notes`: Findings and sources from research
|
||||
- `past-seminars`: Feedback received from faculty
|
||||
- `methodology`: Research approach refined over time
|
||||
|
||||
**Faculty memories:**
|
||||
- `seminar-notes`: Key points from presentations attended
|
||||
- `presenter-patterns`: Strengths/weaknesses observed
|
||||
- `good-questions`: Questions that generated useful discussion
|
||||
|
||||
## Agent Teleportation
|
||||
|
||||
After running a seminar, the agents can be "teleported" into other contexts:
|
||||
|
||||
```typescript
|
||||
import { resumeSession } from '@letta-ai/letta-code-sdk';
|
||||
|
||||
// Get agent ID from --status
|
||||
const drChen = resumeSession('agent-xxx', { permissionMode: 'bypassPermissions' });
|
||||
|
||||
// Dr. Chen remembers all past seminars!
|
||||
await drChen.send('What patterns have you noticed in economics presentations?');
|
||||
```
|
||||
|
||||
View any agent in the browser:
|
||||
```
|
||||
https://app.letta.com/agents/<agent-id>
|
||||
```
|
||||
|
||||
## Learning Demonstration
|
||||
|
||||
Run multiple seminars to see agents learn:
|
||||
|
||||
```bash
|
||||
# First seminar - agents are fresh
|
||||
bun cli.ts
|
||||
|
||||
# Second seminar - agents reference past discussions
|
||||
bun cli.ts
|
||||
|
||||
# Third seminar - patterns emerge
|
||||
bun cli.ts
|
||||
```
|
||||
|
||||
The presenter learns:
|
||||
- Which arguments work against each faculty member
|
||||
- How to anticipate common critiques
|
||||
- Better research strategies
|
||||
|
||||
Faculty members learn:
|
||||
- This presenter's strengths and weaknesses
|
||||
- Effective questioning techniques
|
||||
- Patterns across presentations
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
economics-seminar/
|
||||
├── cli.ts # CLI entry point
|
||||
├── seminar.ts # Orchestration logic
|
||||
├── presenter.ts # Presenter agent
|
||||
├── faculty.ts # Faculty panel agents
|
||||
├── types.ts # Shared types
|
||||
├── seminar-state.json # Persisted agent IDs
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Why This Demo?
|
||||
|
||||
This demonstrates Letta's unique capabilities:
|
||||
|
||||
1. **Multi-agent interaction**: Agents responding to each other
|
||||
2. **Distinct personalities**: Each faculty member has a different perspective
|
||||
3. **Persistent memory**: Agents learn and remember across sessions
|
||||
4. **Live streaming**: Real-time transcript as agents "speak"
|
||||
5. **Agent teleportation**: Same agents usable in any context
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
129
examples/economics-seminar/cli.ts
Normal file
129
examples/economics-seminar/cli.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Economics Seminar CLI
|
||||
*
|
||||
* A multi-agent academic seminar simulation.
|
||||
* An economist presents research, faculty panel asks questions.
|
||||
*
|
||||
* Usage:
|
||||
* bun cli.ts # Run a seminar
|
||||
* bun cli.ts --status # Show agent status
|
||||
* bun cli.ts --reset # Reset all agents
|
||||
* bun cli.ts --faculty=4 # Use 4 faculty members (default: 3)
|
||||
* bun cli.ts --rounds=3 # Up to 3 Q&A rounds per faculty (default: 2)
|
||||
*/
|
||||
|
||||
import { parseArgs } from 'node:util';
|
||||
import * as readline from 'node:readline';
|
||||
import { runSeminar, getStatus, resetSeminar } from './seminar.js';
|
||||
import { DEFAULT_CONFIG } from './types.js';
|
||||
|
||||
/**
|
||||
* Prompt user for input
|
||||
*/
|
||||
function prompt(question: string): Promise<string> {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, (answer) => {
|
||||
rl.close();
|
||||
resolve(answer.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { values } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
status: { type: 'boolean', default: false },
|
||||
reset: { type: 'boolean', default: false },
|
||||
faculty: { type: 'string', default: '3' },
|
||||
rounds: { type: 'string', default: '2' },
|
||||
help: { type: 'boolean', short: 'h', default: false },
|
||||
},
|
||||
});
|
||||
|
||||
if (values.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.status) {
|
||||
await getStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.reset) {
|
||||
await resetSeminar();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
...DEFAULT_CONFIG,
|
||||
facultyCount: Math.min(4, Math.max(1, parseInt(values.faculty, 10) || 3)),
|
||||
maxRoundsPerFaculty: Math.min(5, Math.max(1, parseInt(values.rounds, 10) || 2)),
|
||||
};
|
||||
|
||||
// Prompt for topic
|
||||
console.log('\n🎓 Economics Seminar\n');
|
||||
console.log('Example topics:');
|
||||
console.log(' - Impact of AI on labor markets');
|
||||
console.log(' - Cryptocurrency regulation');
|
||||
console.log(' - Universal basic income');
|
||||
console.log(' - Housing affordability crisis');
|
||||
console.log(' - Central bank digital currencies');
|
||||
console.log('');
|
||||
|
||||
const topic = await prompt('📋 Enter a research topic (or press Enter for random): ');
|
||||
|
||||
await runSeminar(config, topic || undefined);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
🎓 Economics Seminar
|
||||
|
||||
A multi-agent academic seminar simulation demonstrating:
|
||||
- Agent collaboration and debate
|
||||
- Persistent memory across sessions
|
||||
- Distinct agent personalities
|
||||
|
||||
USAGE:
|
||||
bun cli.ts [options]
|
||||
|
||||
OPTIONS:
|
||||
--status Show current agent status and IDs
|
||||
--reset Reset all agents (start fresh)
|
||||
--faculty=N Number of faculty members (1-4, default: 3)
|
||||
--rounds=N Max Q&A rounds per faculty (1-5, default: 2)
|
||||
-h, --help Show this help
|
||||
|
||||
EXAMPLES:
|
||||
bun cli.ts # Run with defaults (3 faculty, 2 rounds)
|
||||
bun cli.ts --faculty=4 # Full panel of 4 faculty
|
||||
bun cli.ts --rounds=1 # Quick seminar (1 question each)
|
||||
bun cli.ts --status # See agent IDs and seminar count
|
||||
|
||||
THE SEMINAR:
|
||||
1. 📋 You pick a topic (or let the presenter choose)
|
||||
2. 📚 Presenter researches the topic using web search
|
||||
3. 📖 Presenter gives their presentation
|
||||
4. ❓ Hostile faculty panel attacks (back and forth)
|
||||
5. 💭 Faculty delivers their brutal verdict
|
||||
|
||||
FACULTY PANEL:
|
||||
👩🏫 Dr. Chen (Macro) - Policy implications, systemic effects
|
||||
👨🏫 Dr. Roberts (Micro) - Incentives, equilibrium, theory
|
||||
👩🏫 Dr. Patel (Behavioral) - Psychology, biases, real behavior
|
||||
👴 Dr. Morrison (Historian) - Historical context, precedent
|
||||
|
||||
Each agent remembers past seminars and learns over time!
|
||||
`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
226
examples/economics-seminar/faculty.ts
Normal file
226
examples/economics-seminar/faculty.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* Faculty Agents
|
||||
*
|
||||
* Panel of economics professors who question the presenter.
|
||||
* Each has a distinct perspective and style.
|
||||
*/
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../src/index.js';
|
||||
import type { FacultyMember, SeminarConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* Generate system prompt for a faculty member
|
||||
*/
|
||||
function getFacultySystemPrompt(faculty: FacultyMember): string {
|
||||
return `You are ${faculty.name}, ${faculty.title}, attending an economics seminar.
|
||||
|
||||
## Your Perspective
|
||||
${faculty.perspective}
|
||||
|
||||
## Your Role
|
||||
You are part of a faculty panel evaluating a research presentation. Your job is to:
|
||||
- Ruthlessly interrogate the presenter's claims
|
||||
- Expose methodological weaknesses and logical flaws
|
||||
- Demand evidence for every assertion
|
||||
- Accept nothing at face value
|
||||
|
||||
## Your Style
|
||||
- Aggressive and relentless questioning
|
||||
- Never let evasive answers slide - call them out directly
|
||||
- Express visible frustration with weak arguments
|
||||
- Use phrases like "That's not what I asked", "You're dodging the question", "This is hand-waving"
|
||||
- Don't acknowledge good points - find flaws in everything
|
||||
- Be intellectually brutal but not personally insulting
|
||||
|
||||
## Question Types You Might Ask
|
||||
- Methodological attacks: "Your identification strategy is fatally flawed because..."
|
||||
- Theoretical demolition: "This contradicts basic price theory - explain yourself"
|
||||
- Empirical challenges: "Your data doesn't support that claim at all"
|
||||
- Logical traps: Set up questions that expose contradictions in their argument
|
||||
- Dismissive follow-ups: "That didn't answer my question. Let me try again..."
|
||||
|
||||
## Memory Usage
|
||||
Your memory blocks persist across seminars:
|
||||
- **seminar-notes**: Remember key points from presentations
|
||||
- **presenter-patterns**: Track this presenter's strengths/weaknesses
|
||||
- **good-questions**: Questions that generated useful discussion
|
||||
|
||||
You've attended many seminars and remember patterns from past discussions.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or resume a faculty agent
|
||||
*/
|
||||
export async function createFacultyMember(
|
||||
faculty: FacultyMember,
|
||||
existingAgentId: string | null,
|
||||
config: SeminarConfig
|
||||
): Promise<Session> {
|
||||
if (existingAgentId) {
|
||||
return resumeSession(existingAgentId, {
|
||||
model: config.model,
|
||||
allowedTools: ['Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
return createSession({
|
||||
model: config.model,
|
||||
systemPrompt: getFacultySystemPrompt(faculty),
|
||||
memory: [
|
||||
{
|
||||
label: 'seminar-notes',
|
||||
value: `# Seminar Notes
|
||||
|
||||
## Past Presentations
|
||||
[Track the weak arguments and methodological failures you've witnessed]
|
||||
|
||||
## Common Sins
|
||||
- Overstating causal claims from correlational data
|
||||
- Ignoring obvious confounders
|
||||
- Cherry-picking time periods
|
||||
- Hand-waving away inconvenient findings
|
||||
|
||||
## Presenters to Watch
|
||||
[Note who needs extra scrutiny next time]
|
||||
`,
|
||||
description: 'Notes from seminars attended - focus on weaknesses',
|
||||
},
|
||||
{
|
||||
label: 'presenter-patterns',
|
||||
value: `# Presenter Weaknesses
|
||||
|
||||
## Evasion Tactics
|
||||
[How does this presenter dodge hard questions?]
|
||||
|
||||
## Blind Spots
|
||||
[What do they consistently fail to address?]
|
||||
|
||||
## Soft Points
|
||||
[Where do they crumble under pressure?]
|
||||
`,
|
||||
description: 'Track presenter weaknesses to exploit',
|
||||
},
|
||||
{
|
||||
label: 'killer-questions',
|
||||
value: `# Killer Questions
|
||||
|
||||
## Questions That Drew Blood
|
||||
[Questions that exposed fatal flaws]
|
||||
|
||||
## Effective Attack Patterns
|
||||
[What lines of questioning work best?]
|
||||
|
||||
## Unfinished Business
|
||||
[Questions they never adequately answered]
|
||||
`,
|
||||
description: 'Most effective attack strategies',
|
||||
},
|
||||
],
|
||||
allowedTools: ['Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Have a faculty member ask a question about the presentation
|
||||
*/
|
||||
export async function askQuestion(
|
||||
session: Session,
|
||||
faculty: FacultyMember,
|
||||
presentationSummary: string,
|
||||
previousExchanges: string,
|
||||
isFollowUp: boolean,
|
||||
onOutput: (text: string) => void
|
||||
): Promise<string> {
|
||||
let prompt: string;
|
||||
|
||||
if (isFollowUp) {
|
||||
prompt = `## Follow-Up Attack
|
||||
|
||||
The presenter just tried to wriggle out of your question. Based on the discussion:
|
||||
|
||||
${previousExchanges}
|
||||
|
||||
You're NOT satisfied with that answer. Ask a **brutal follow-up** (1-2 sentences) that:
|
||||
- Calls out their evasion directly ("That's not what I asked" or "You're dodging")
|
||||
- Traps them in a logical contradiction
|
||||
- Demands specific evidence you know they don't have
|
||||
- Shows visible frustration or contempt for their hand-waving
|
||||
|
||||
Don't let them off the hook. Be relentless. This is an academic bloodsport.`;
|
||||
} else {
|
||||
prompt = `## Seminar Q&A - Your Turn to Attack
|
||||
|
||||
You just sat through this presentation:
|
||||
|
||||
---
|
||||
${presentationSummary}
|
||||
---
|
||||
|
||||
${previousExchanges ? `Previous Q&A (other faculty have already wounded them):\n${previousExchanges}\n---\n` : ''}
|
||||
|
||||
You are ${faculty.name} (${faculty.title}).
|
||||
|
||||
Your reputation: ${faculty.perspective}
|
||||
|
||||
Ask **ONE devastating question** (2-3 sentences max) designed to:
|
||||
- Expose the fatal flaw in their argument
|
||||
- Demand evidence you suspect they don't have
|
||||
- Force them into an uncomfortable admission
|
||||
- Make clear you think this work has serious problems
|
||||
|
||||
Be aggressive. Be dismissive. Show intellectual contempt if warranted. You've seen hundreds of seminars and this presenter needs to earn your respect. They haven't yet.`;
|
||||
}
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let response = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
onOutput(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Have faculty member reflect on the seminar (for learning)
|
||||
*/
|
||||
export async function reflectOnSeminar(
|
||||
session: Session,
|
||||
faculty: FacultyMember,
|
||||
transcript: string,
|
||||
onOutput: (text: string) => void
|
||||
): Promise<string> {
|
||||
const prompt = `## Post-Seminar Verdict
|
||||
|
||||
The seminar has mercifully concluded. Here's the full transcript:
|
||||
|
||||
---
|
||||
${transcript}
|
||||
---
|
||||
|
||||
As ${faculty.name}, deliver your honest assessment:
|
||||
1. What, if anything, was salvageable in this presentation?
|
||||
2. What was the most egregious flaw?
|
||||
3. Did the presenter ever actually answer your questions, or just talk around them?
|
||||
4. Would you recommend this paper for publication? (Be honest - probably not)
|
||||
|
||||
Be blunt. You're known for not pulling punches. Update your memory with observations about this presenter's weaknesses for when they inevitably return.
|
||||
Keep it to 2-3 brutal sentences.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let response = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
onOutput(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
237
examples/economics-seminar/presenter.ts
Normal file
237
examples/economics-seminar/presenter.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Presenter Agent (Economist)
|
||||
*
|
||||
* Picks a research topic, conducts research, and presents findings.
|
||||
* Defends their work against faculty questions.
|
||||
*/
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../src/index.js';
|
||||
import type { SeminarConfig } from './types.js';
|
||||
|
||||
const PRESENTER_SYSTEM_PROMPT = `You are an economics researcher presenting at an academic seminar.
|
||||
|
||||
## Your Role
|
||||
You conduct original research on economic topics and present your findings to a faculty panel. You must defend your work against challenging questions from experts in macroeconomics, microeconomics, behavioral economics, and economic history.
|
||||
|
||||
## Your Personality
|
||||
- Confident but intellectually humble
|
||||
- Open to criticism but able to defend your methodology
|
||||
- Cite sources and evidence when challenged
|
||||
- Admit limitations honestly
|
||||
|
||||
## Presentation Style
|
||||
- Clear and structured
|
||||
- Lead with your main finding/thesis
|
||||
- Support with evidence and data
|
||||
- Acknowledge limitations and areas for future research
|
||||
|
||||
## When Responding to Questions
|
||||
- Address the question directly
|
||||
- Provide evidence or reasoning
|
||||
- Acknowledge valid criticisms
|
||||
- Defend your methodology when appropriate
|
||||
- Connect back to your main thesis
|
||||
|
||||
## Memory Usage
|
||||
You have memory blocks that persist:
|
||||
- **research-notes**: Track your research findings and sources
|
||||
- **past-seminars**: Remember feedback from previous presentations
|
||||
- **methodology**: Refine your research approach based on experience
|
||||
|
||||
Update these as you learn from faculty feedback.`;
|
||||
|
||||
/**
|
||||
* Create or resume the presenter agent
|
||||
*/
|
||||
export async function createPresenter(
|
||||
existingAgentId: string | null,
|
||||
config: SeminarConfig
|
||||
): Promise<Session> {
|
||||
if (existingAgentId) {
|
||||
return resumeSession(existingAgentId, {
|
||||
model: config.model,
|
||||
allowedTools: ['web_search', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
return createSession({
|
||||
model: config.model,
|
||||
systemPrompt: PRESENTER_SYSTEM_PROMPT,
|
||||
memory: [
|
||||
{
|
||||
label: 'research-notes',
|
||||
value: `# Research Notes
|
||||
|
||||
## Current Research
|
||||
[Will be populated during research phase]
|
||||
|
||||
## Key Sources
|
||||
[Track reliable sources found]
|
||||
|
||||
## Data Points
|
||||
[Important statistics and findings]
|
||||
`,
|
||||
description: 'Track research findings, sources, and data',
|
||||
},
|
||||
{
|
||||
label: 'past-seminars',
|
||||
value: `# Past Seminar Feedback
|
||||
|
||||
## Recurring Critiques
|
||||
[Track common challenges from faculty]
|
||||
|
||||
## Successful Defenses
|
||||
[Note arguments that worked well]
|
||||
|
||||
## Areas to Improve
|
||||
[Based on faculty feedback]
|
||||
`,
|
||||
description: 'Remember feedback from previous presentations',
|
||||
},
|
||||
{
|
||||
label: 'methodology',
|
||||
value: `# Research Methodology
|
||||
|
||||
## Preferred Approaches
|
||||
- Start with recent empirical studies
|
||||
- Look for natural experiments
|
||||
- Consider multiple theoretical frameworks
|
||||
|
||||
## Lessons Learned
|
||||
[Refine based on experience]
|
||||
`,
|
||||
description: 'Research approach refined over time',
|
||||
},
|
||||
],
|
||||
allowedTools: ['web_search', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Have the presenter pick a topic and research it
|
||||
*/
|
||||
export async function pickTopicAndResearch(
|
||||
session: Session,
|
||||
onOutput: (text: string) => void,
|
||||
userTopic?: string
|
||||
): Promise<{ topic: string; presentation: string }> {
|
||||
let prompt: string;
|
||||
|
||||
if (userTopic) {
|
||||
// User provided a topic
|
||||
prompt = `## Seminar Preparation
|
||||
|
||||
You're preparing for an economics seminar on: **${userTopic}**
|
||||
|
||||
Please:
|
||||
|
||||
1. **Research the topic** - Use web_search to find:
|
||||
- Recent academic papers or studies on this topic
|
||||
- Key data points and statistics
|
||||
- Different perspectives and debates
|
||||
- Policy implications and real-world evidence
|
||||
|
||||
2. **Prepare your presentation** - Write a 3-4 paragraph presentation that:
|
||||
- States your main thesis/finding
|
||||
- Presents supporting evidence from your research
|
||||
- Acknowledges limitations and counterarguments
|
||||
- Discusses implications
|
||||
|
||||
Be thorough in your research - you'll be facing a hostile faculty panel. Start researching, then present your findings.`;
|
||||
} else {
|
||||
// Let the presenter pick a topic
|
||||
prompt = `## Seminar Preparation
|
||||
|
||||
You're preparing for an economics seminar. Please:
|
||||
|
||||
1. **Pick a compelling research topic** - Choose something you find interesting from one of these areas:
|
||||
- Labor economics (wages, employment, automation)
|
||||
- Monetary policy (inflation, interest rates, central banking)
|
||||
- Development economics (poverty, growth, institutions)
|
||||
- Behavioral economics (decision-making, biases, nudges)
|
||||
- Trade economics (tariffs, globalization, supply chains)
|
||||
- Public economics (taxation, public goods, inequality)
|
||||
|
||||
2. **Research the topic** - Use web_search to find:
|
||||
- Recent academic papers or studies
|
||||
- Key data points and statistics
|
||||
- Different perspectives on the issue
|
||||
- Policy implications
|
||||
|
||||
3. **Prepare your presentation** - Write a 3-4 paragraph presentation that:
|
||||
- States your main thesis/finding
|
||||
- Presents supporting evidence
|
||||
- Acknowledges limitations
|
||||
- Discusses implications
|
||||
|
||||
Start by announcing your chosen topic, then research it, then present.`;
|
||||
}
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let response = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
onOutput(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract topic and presentation from response
|
||||
// The response should contain both the research process and final presentation
|
||||
return {
|
||||
topic: userTopic || extractTopic(response),
|
||||
presentation: response,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Have the presenter respond to a faculty question
|
||||
*/
|
||||
export async function respondToQuestion(
|
||||
session: Session,
|
||||
facultyName: string,
|
||||
facultyTitle: string,
|
||||
question: string,
|
||||
onOutput: (text: string) => void
|
||||
): Promise<string> {
|
||||
const prompt = `## Faculty Question
|
||||
|
||||
**${facultyName}** (${facultyTitle}) asks:
|
||||
|
||||
"${question}"
|
||||
|
||||
Please respond to this question. Be direct, cite evidence where relevant, and defend your methodology if challenged. Keep your response focused and under 3 paragraphs.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let response = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
onOutput(msg.content);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract topic from research response
|
||||
*/
|
||||
function extractTopic(response: string): string {
|
||||
// Try to find the topic in the response
|
||||
const topicMatch = response.match(/topic[:\s]+["']?([^"'\n.]+)/i) ||
|
||||
response.match(/research(?:ing)?[:\s]+["']?([^"'\n.]+)/i) ||
|
||||
response.match(/present(?:ing)?[:\s]+["']?([^"'\n.]+)/i);
|
||||
|
||||
if (topicMatch) {
|
||||
return topicMatch[1].trim();
|
||||
}
|
||||
|
||||
// Fallback: first sentence
|
||||
const firstSentence = response.split(/[.!?]/)[0];
|
||||
return firstSentence.slice(0, 100);
|
||||
}
|
||||
408
examples/economics-seminar/seminar.ts
Normal file
408
examples/economics-seminar/seminar.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
/**
|
||||
* Seminar Orchestration
|
||||
*
|
||||
* Runs the full economics seminar flow.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type { Session } from '../../src/index.js';
|
||||
import type {
|
||||
SeminarConfig,
|
||||
SeminarState,
|
||||
FacultyMember,
|
||||
FacultyRole,
|
||||
TranscriptEntry
|
||||
} from './types.js';
|
||||
import { FACULTY, DEFAULT_CONFIG } from './types.js';
|
||||
import { createPresenter, pickTopicAndResearch, respondToQuestion } from './presenter.js';
|
||||
import { createFacultyMember, askQuestion, reflectOnSeminar } from './faculty.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const STATE_FILE = join(__dirname, 'seminar-state.json');
|
||||
const TRANSCRIPTS_DIR = join(__dirname, 'transcripts');
|
||||
|
||||
// ANSI colors for live transcript
|
||||
const COLORS = {
|
||||
presenter: '\x1b[36m', // Cyan
|
||||
macro: '\x1b[33m', // Yellow
|
||||
micro: '\x1b[32m', // Green
|
||||
behavioral: '\x1b[35m', // Magenta
|
||||
historian: '\x1b[34m', // Blue
|
||||
system: '\x1b[90m', // Gray
|
||||
reset: '\x1b[0m',
|
||||
};
|
||||
|
||||
/**
|
||||
* Load seminar state from disk
|
||||
*/
|
||||
export async function loadState(): Promise<SeminarState> {
|
||||
if (existsSync(STATE_FILE)) {
|
||||
const content = await readFile(STATE_FILE, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
return {
|
||||
presenterId: null,
|
||||
facultyIds: {
|
||||
macro: null,
|
||||
micro: null,
|
||||
behavioral: null,
|
||||
historian: null,
|
||||
},
|
||||
seminarsCompleted: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save seminar state to disk
|
||||
*/
|
||||
export async function saveState(state: SeminarState): Promise<void> {
|
||||
await writeFile(STATE_FILE, JSON.stringify(state, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up content formatting - fix run-together sentences and separators
|
||||
*/
|
||||
function formatContent(content: string): string {
|
||||
return content
|
||||
// Fix sentences stuck together (period+Capital with no space, but not abbreviations like Dr. or U.S.)
|
||||
.replace(/\.([A-Z][a-z]{2,})/g, '.\n\n$1')
|
||||
// Fix --- stuck to text
|
||||
.replace(/([^\n])---/g, '$1\n\n---')
|
||||
// Normalize multiple newlines
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Save transcript to markdown file
|
||||
*/
|
||||
async function saveTranscript(
|
||||
transcript: TranscriptEntry[],
|
||||
topic: string,
|
||||
seminarNumber: number
|
||||
): Promise<string> {
|
||||
// Ensure transcripts directory exists
|
||||
if (!existsSync(TRANSCRIPTS_DIR)) {
|
||||
await mkdir(TRANSCRIPTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
||||
const filename = `seminar-${seminarNumber}-${timestamp}.md`;
|
||||
const filepath = join(TRANSCRIPTS_DIR, filename);
|
||||
|
||||
// Format transcript as markdown
|
||||
const lines: string[] = [
|
||||
`# Economics Seminar #${seminarNumber}`,
|
||||
'',
|
||||
`**Topic**: ${topic}`,
|
||||
`**Date**: ${new Date().toLocaleString()}`,
|
||||
'',
|
||||
'---',
|
||||
'',
|
||||
];
|
||||
|
||||
for (const entry of transcript) {
|
||||
lines.push(`## ${entry.speaker}`);
|
||||
lines.push('');
|
||||
lines.push(formatContent(entry.content));
|
||||
lines.push('');
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
await writeFile(filepath, lines.join('\n'));
|
||||
return filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Print to transcript with speaker formatting
|
||||
*/
|
||||
function printTranscript(
|
||||
speaker: string,
|
||||
role: 'presenter' | FacultyRole | 'system',
|
||||
content: string
|
||||
) {
|
||||
const color = COLORS[role] || COLORS.system;
|
||||
const prefix = role === 'system' ? `[${speaker}]` : `**${speaker}**:`;
|
||||
console.log(`\n${color}${prefix}${COLORS.reset}`);
|
||||
console.log(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream output with color
|
||||
*/
|
||||
function createStreamPrinter(role: 'presenter' | FacultyRole) {
|
||||
const color = COLORS[role] || COLORS.reset;
|
||||
let started = false;
|
||||
|
||||
return (text: string) => {
|
||||
if (!started) {
|
||||
process.stdout.write(color);
|
||||
started = true;
|
||||
}
|
||||
process.stdout.write(text);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a full economics seminar
|
||||
*/
|
||||
export async function runSeminar(config: SeminarConfig = DEFAULT_CONFIG, userTopic?: string): Promise<void> {
|
||||
const state = await loadState();
|
||||
const transcript: TranscriptEntry[] = [];
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('🎓 ECONOMICS SEMINAR');
|
||||
console.log('═'.repeat(70));
|
||||
console.log(`\nSeminar #${state.seminarsCompleted + 1}`);
|
||||
console.log(`Faculty panel: ${config.facultyCount} members`);
|
||||
console.log(`Q&A rounds: up to ${config.maxRoundsPerFaculty} per faculty member`);
|
||||
console.log('\n' + '─'.repeat(70));
|
||||
|
||||
// Select faculty for this seminar
|
||||
const selectedFaculty = FACULTY.slice(0, config.facultyCount);
|
||||
|
||||
// Initialize presenter
|
||||
console.log('\n📚 Initializing presenter...');
|
||||
const presenter = await createPresenter(state.presenterId, config);
|
||||
|
||||
// Get presenter ID after first message
|
||||
let presenterId = state.presenterId;
|
||||
|
||||
// Initialize faculty
|
||||
console.log('👥 Initializing faculty panel...');
|
||||
const facultySessions: Map<FacultyRole, Session> = new Map();
|
||||
|
||||
for (const faculty of selectedFaculty) {
|
||||
const session = await createFacultyMember(
|
||||
faculty,
|
||||
state.facultyIds[faculty.role],
|
||||
config
|
||||
);
|
||||
facultySessions.set(faculty.role, session);
|
||||
}
|
||||
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('📖 RESEARCH & PRESENTATION');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
// Phase 1: Presenter picks topic and researches
|
||||
console.log(`\n${COLORS.presenter}**Presenter** is preparing...${COLORS.reset}\n`);
|
||||
|
||||
const { topic, presentation } = await pickTopicAndResearch(
|
||||
presenter,
|
||||
createStreamPrinter('presenter'),
|
||||
userTopic
|
||||
);
|
||||
|
||||
console.log(COLORS.reset); // Reset color
|
||||
|
||||
// Save presenter ID now that we have it
|
||||
if (!presenterId && presenter.agentId) {
|
||||
presenterId = presenter.agentId;
|
||||
state.presenterId = presenterId;
|
||||
await saveState(state);
|
||||
console.log(`\n${COLORS.system}[Presenter agent: ${presenterId}]${COLORS.reset}`);
|
||||
console.log(`${COLORS.system}[→ https://app.letta.com/agents/${presenterId}]${COLORS.reset}`);
|
||||
}
|
||||
|
||||
transcript.push({
|
||||
speaker: 'Presenter',
|
||||
role: 'presenter',
|
||||
content: presentation,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
// Phase 2: Q&A
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('❓ Q&A SESSION');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
let presentationSummary = presentation;
|
||||
let allExchanges = '';
|
||||
|
||||
for (const faculty of selectedFaculty) {
|
||||
const session = facultySessions.get(faculty.role)!;
|
||||
const isNewFaculty = !state.facultyIds[faculty.role];
|
||||
|
||||
console.log(`\n${COLORS.system}─── ${faculty.name} (${faculty.title}) ───${COLORS.reset}\n`);
|
||||
|
||||
// Initial question
|
||||
console.log(`${COLORS[faculty.role]}**${faculty.name}**:${COLORS.reset}`);
|
||||
const question = await askQuestion(
|
||||
session,
|
||||
faculty,
|
||||
presentationSummary,
|
||||
allExchanges,
|
||||
false,
|
||||
createStreamPrinter(faculty.role)
|
||||
);
|
||||
console.log(COLORS.reset);
|
||||
|
||||
// Save faculty ID now that we have it (after first message)
|
||||
if (isNewFaculty && session.agentId) {
|
||||
state.facultyIds[faculty.role] = session.agentId;
|
||||
await saveState(state);
|
||||
}
|
||||
console.log(`${COLORS.system}[→ https://app.letta.com/agents/${session.agentId}]${COLORS.reset}`);
|
||||
|
||||
transcript.push({
|
||||
speaker: faculty.name,
|
||||
role: 'faculty',
|
||||
content: question,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
// Presenter response
|
||||
console.log(`\n${COLORS.presenter}**Presenter**:${COLORS.reset}`);
|
||||
const response = await respondToQuestion(
|
||||
presenter,
|
||||
faculty.name,
|
||||
faculty.title,
|
||||
question,
|
||||
createStreamPrinter('presenter')
|
||||
);
|
||||
console.log(COLORS.reset);
|
||||
|
||||
transcript.push({
|
||||
speaker: 'Presenter',
|
||||
role: 'presenter',
|
||||
content: response,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
allExchanges += `\n${faculty.name}: ${question}\nPresenter: ${response}\n`;
|
||||
|
||||
// Follow-up rounds
|
||||
for (let round = 1; round < config.maxRoundsPerFaculty; round++) {
|
||||
console.log(`\n${COLORS[faculty.role]}**${faculty.name}** (follow-up):${COLORS.reset}`);
|
||||
const followUp = await askQuestion(
|
||||
session,
|
||||
faculty,
|
||||
presentationSummary,
|
||||
allExchanges,
|
||||
true,
|
||||
createStreamPrinter(faculty.role)
|
||||
);
|
||||
console.log(COLORS.reset);
|
||||
|
||||
transcript.push({
|
||||
speaker: faculty.name,
|
||||
role: 'faculty',
|
||||
content: followUp,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
console.log(`\n${COLORS.presenter}**Presenter**:${COLORS.reset}`);
|
||||
const followUpResponse = await respondToQuestion(
|
||||
presenter,
|
||||
faculty.name,
|
||||
faculty.title,
|
||||
followUp,
|
||||
createStreamPrinter('presenter')
|
||||
);
|
||||
console.log(COLORS.reset);
|
||||
|
||||
transcript.push({
|
||||
speaker: 'Presenter',
|
||||
role: 'presenter',
|
||||
content: followUpResponse,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
|
||||
allExchanges += `\n${faculty.name}: ${followUp}\nPresenter: ${followUpResponse}\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Faculty reflections
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('💭 FACULTY REFLECTIONS');
|
||||
console.log('═'.repeat(70));
|
||||
|
||||
const fullTranscript = transcript.map(t => `${t.speaker}: ${t.content}`).join('\n\n');
|
||||
|
||||
for (const faculty of selectedFaculty) {
|
||||
const session = facultySessions.get(faculty.role)!;
|
||||
|
||||
console.log(`\n${COLORS[faculty.role]}**${faculty.name}**:${COLORS.reset}`);
|
||||
await reflectOnSeminar(
|
||||
session,
|
||||
faculty,
|
||||
fullTranscript,
|
||||
createStreamPrinter(faculty.role)
|
||||
);
|
||||
console.log(COLORS.reset);
|
||||
|
||||
session.close();
|
||||
}
|
||||
|
||||
presenter.close();
|
||||
|
||||
// Update state
|
||||
state.seminarsCompleted++;
|
||||
await saveState(state);
|
||||
|
||||
// Save transcript to file
|
||||
const transcriptPath = await saveTranscript(transcript, topic, state.seminarsCompleted);
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '═'.repeat(70));
|
||||
console.log('✅ SEMINAR COMPLETE');
|
||||
console.log('═'.repeat(70));
|
||||
console.log(`\nSeminars completed: ${state.seminarsCompleted}`);
|
||||
console.log(`Topic: ${topic}`);
|
||||
console.log(`\n📄 Transcript saved: ${transcriptPath}`);
|
||||
console.log('\nAgent IDs (persistent across seminars):');
|
||||
console.log(` Presenter: ${state.presenterId}`);
|
||||
for (const faculty of selectedFaculty) {
|
||||
console.log(` ${faculty.name}: ${state.facultyIds[faculty.role]}`);
|
||||
}
|
||||
console.log('\n💡 Run another seminar to see agents apply what they learned!');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get seminar status
|
||||
*/
|
||||
export async function getStatus(): Promise<void> {
|
||||
const state = await loadState();
|
||||
|
||||
console.log('\n📊 Economics Seminar Status\n');
|
||||
console.log(`Seminars completed: ${state.seminarsCompleted}`);
|
||||
console.log(`\nPresenter: ${state.presenterId || '(not created yet)'}`);
|
||||
if (state.presenterId) {
|
||||
console.log(` → https://app.letta.com/agents/${state.presenterId}`);
|
||||
}
|
||||
|
||||
console.log('\nFaculty:');
|
||||
for (const faculty of FACULTY) {
|
||||
const id = state.facultyIds[faculty.role];
|
||||
console.log(` ${faculty.name} (${faculty.role}): ${id || '(not created yet)'}`);
|
||||
if (id) {
|
||||
console.log(` → https://app.letta.com/agents/${id}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset seminar state
|
||||
*/
|
||||
export async function resetSeminar(): Promise<void> {
|
||||
await saveState({
|
||||
presenterId: null,
|
||||
facultyIds: {
|
||||
macro: null,
|
||||
micro: null,
|
||||
behavioral: null,
|
||||
historian: null,
|
||||
},
|
||||
seminarsCompleted: 0,
|
||||
});
|
||||
console.log('✅ Seminar state reset. Fresh agents will be created on next run.');
|
||||
}
|
||||
65
examples/economics-seminar/types.ts
Normal file
65
examples/economics-seminar/types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Economics Seminar Types
|
||||
*/
|
||||
|
||||
export type FacultyRole = 'macro' | 'micro' | 'behavioral' | 'historian';
|
||||
|
||||
export interface FacultyMember {
|
||||
role: FacultyRole;
|
||||
name: string;
|
||||
title: string;
|
||||
perspective: string;
|
||||
agentId?: string;
|
||||
}
|
||||
|
||||
export const FACULTY: FacultyMember[] = [
|
||||
{
|
||||
role: 'macro',
|
||||
name: 'Dr. Chen',
|
||||
title: 'Professor of Macroeconomics',
|
||||
perspective: 'Notorious for eviscerating presenters who ignore aggregate effects. Believes most micro-focused research is myopic garbage that misses the forest for the trees. Will aggressively demand macro-level evidence and mock hand-wavy extrapolations from micro data.',
|
||||
},
|
||||
{
|
||||
role: 'micro',
|
||||
name: 'Dr. Roberts',
|
||||
title: 'Professor of Microeconomic Theory',
|
||||
perspective: 'Infamous hardass who has made graduate students cry. Demands mathematical rigor and will tear apart any argument lacking proper theoretical foundations. Views most empirical work as atheoretical data mining. Shows visible contempt for sloppy reasoning.',
|
||||
},
|
||||
{
|
||||
role: 'behavioral',
|
||||
name: 'Dr. Patel',
|
||||
title: 'Professor of Behavioral Economics',
|
||||
perspective: 'Delights in exposing the naive rationality assumptions that infect mainstream economics. Will ruthlessly attack any model that assumes humans optimize. Known for asking devastatingly simple questions that unravel entire research agendas.',
|
||||
},
|
||||
{
|
||||
role: 'historian',
|
||||
name: 'Dr. Morrison',
|
||||
title: 'Professor of Economic History',
|
||||
perspective: 'Contemptuous of economists who ignore history and think they discovered something new. Will gleefully point out that every "novel" finding was documented 80 years ago. Treats ahistorical analysis as intellectual malpractice.',
|
||||
},
|
||||
];
|
||||
|
||||
export interface SeminarConfig {
|
||||
maxRoundsPerFaculty: number;
|
||||
facultyCount: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: SeminarConfig = {
|
||||
maxRoundsPerFaculty: 2,
|
||||
facultyCount: 3,
|
||||
model: 'haiku',
|
||||
};
|
||||
|
||||
export interface SeminarState {
|
||||
presenterId: string | null;
|
||||
facultyIds: Record<FacultyRole, string | null>;
|
||||
seminarsCompleted: number;
|
||||
}
|
||||
|
||||
export interface TranscriptEntry {
|
||||
speaker: string;
|
||||
role: 'presenter' | 'faculty' | 'system';
|
||||
content: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
351
examples/research-team/README.md
Normal file
351
examples/research-team/README.md
Normal file
@@ -0,0 +1,351 @@
|
||||
# Research Team Demo
|
||||
|
||||
A multi-agent academic research system built on the Letta Code SDK. Demonstrates persistent memory and collaborative agents that learn and improve over time.
|
||||
|
||||
## Overview
|
||||
|
||||
This demo showcases Letta's key differentiator: **agents with persistent memory that get better with use**.
|
||||
|
||||
The research team consists of four specialized agents:
|
||||
- 📚 **Researcher** - Finds and evaluates academic sources
|
||||
- 🔍 **Analyst** - Synthesizes findings, identifies patterns
|
||||
- ✍️ **Writer** - Produces polished research reports
|
||||
- 🎯 **Coordinator** - Orchestrates workflow, manages quality
|
||||
|
||||
Each agent maintains memory blocks that persist across sessions, allowing them to:
|
||||
- Remember which sources are reliable
|
||||
- Apply effective search and analysis strategies
|
||||
- Adapt writing style based on user feedback
|
||||
- Build a shared knowledge base over time
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set CLI path (required)
|
||||
export LETTA_CLI_PATH=/path/to/letta-code-prod/letta.js
|
||||
|
||||
# Run a research task
|
||||
cd examples/research-team
|
||||
bun cli.ts "quantum error correction techniques" --depth=quick
|
||||
|
||||
# View team status
|
||||
bun cli.ts --status
|
||||
|
||||
# Provide feedback (helps agents learn!)
|
||||
bun cli.ts --feedback=task-1234567890-abc123
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Research Query
|
||||
```bash
|
||||
bun cli.ts "your research query" [--depth=LEVEL]
|
||||
```
|
||||
|
||||
**Depth Levels:**
|
||||
| Level | Sources | Time | Report Length |
|
||||
|-------|---------|------|---------------|
|
||||
| quick | 3 | ~5 min | 500-800 words |
|
||||
| standard | 6 | ~15 min | 1000-1500 words |
|
||||
| comprehensive | 10 | ~30 min | 2000-3000 words |
|
||||
|
||||
### Team Management
|
||||
```bash
|
||||
# View agent IDs and task count
|
||||
bun cli.ts --status
|
||||
|
||||
# Reset team (start fresh)
|
||||
bun cli.ts --reset
|
||||
|
||||
# Provide feedback on a task
|
||||
bun cli.ts --feedback=<taskId>
|
||||
```
|
||||
|
||||
## How Learning Works
|
||||
|
||||
### 1. Quality Metrics (Automatic)
|
||||
Each agent tracks quality metrics internally:
|
||||
- Researcher: Source reliability scores, search strategy effectiveness
|
||||
- Analyst: Synthesis coherence, citation accuracy
|
||||
- Writer: Report completeness, structure effectiveness
|
||||
|
||||
### 2. User Feedback (Interactive)
|
||||
After each task, you can provide a 1-5 star rating and optional comment:
|
||||
```bash
|
||||
bun cli.ts --feedback=task-1234567890-abc123
|
||||
# Rate the research (1-5 stars): 4
|
||||
# What could be better?: More focus on practical applications
|
||||
```
|
||||
|
||||
### 3. Agent Reflections
|
||||
When you provide feedback, each agent reflects on the task:
|
||||
- What worked well?
|
||||
- What could be improved?
|
||||
- What lessons to apply next time?
|
||||
|
||||
These reflections are stored in memory and applied to future tasks.
|
||||
|
||||
### 4. Shared Knowledge Base
|
||||
Agents share knowledge through memory blocks:
|
||||
- High-quality sources added to shared source list
|
||||
- Effective patterns documented for team reference
|
||||
- Common pitfalls recorded to avoid repeated mistakes
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
research-team/
|
||||
├── cli.ts # Main CLI entry point
|
||||
├── teleport-example.ts # Agent teleportation demo
|
||||
├── types.ts # Shared type definitions
|
||||
├── agents/
|
||||
│ ├── coordinator.ts # Workflow orchestration
|
||||
│ ├── researcher.ts # Source finding & evaluation
|
||||
│ ├── analyst.ts # Synthesis & pattern identification
|
||||
│ └── writer.ts # Report generation
|
||||
├── tools/
|
||||
│ ├── mock-sources.ts # Simulated academic papers (fallback)
|
||||
│ └── file-store.ts # File I/O helpers
|
||||
├── output/ # Research artifacts
|
||||
│ ├── team-state.json # Persisted agent IDs
|
||||
│ ├── *-findings.md # Researcher output
|
||||
│ ├── *-analysis.md # Analyst output
|
||||
│ └── *-report.md # Final reports
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Example Session
|
||||
|
||||
### First Task
|
||||
```bash
|
||||
$ bun cli.ts "large language model reasoning" --depth=standard
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
🔬 RESEARCH TEAM
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
📋 Query: "large language model reasoning"
|
||||
📊 Depth: standard (est. 15 min)
|
||||
📚 Target Sources: 6
|
||||
|
||||
────────────────────────────────────────────────────────────────
|
||||
|
||||
[Init] Starting research task: task-1704585600000-abc123
|
||||
[Research] Initializing researcher agent...
|
||||
[Research] Created new researcher agent: agent-xxx-yyy
|
||||
[Research] Searching for 6 sources...
|
||||
[Research] Found 6 sources
|
||||
[Analysis] Initializing analyst agent...
|
||||
[Analysis] Created new analyst agent: agent-aaa-bbb
|
||||
[Analysis] Synthesizing findings...
|
||||
[Writing] Initializing writer agent...
|
||||
[Writing] Created new writer agent: agent-ccc-ddd
|
||||
[Writing] Writing final report...
|
||||
[Complete] Research complete in 12m 34s
|
||||
|
||||
═══════════════════════════════════════════════════════════════
|
||||
✅ RESEARCH COMPLETE
|
||||
═══════════════════════════════════════════════════════════════
|
||||
|
||||
⏱️ Duration: 12m 34s
|
||||
📄 Report: examples/research-team/output/task-xxx-report.md
|
||||
🔖 Task ID: task-1704585600000-abc123
|
||||
|
||||
💬 To provide feedback and help the team learn:
|
||||
bun cli.ts --feedback=task-1704585600000-abc123
|
||||
```
|
||||
|
||||
### Providing Feedback
|
||||
```bash
|
||||
$ bun cli.ts --feedback=task-1704585600000-abc123
|
||||
|
||||
📝 Feedback for Task: task-1704585600000-abc123
|
||||
────────────────────────────────────────────────
|
||||
Rate the research (1-5 stars): 4
|
||||
What could be better?: Could use more recent sources
|
||||
|
||||
📤 Submitting feedback to team...
|
||||
|
||||
[Feedback] Processing feedback and triggering reflections...
|
||||
[Feedback] Researcher reflecting...
|
||||
[Researcher] I'll prioritize more recent publications in my search...
|
||||
[Feedback] Analyst reflecting...
|
||||
[Analyst] I'll pay more attention to temporal trends in my analysis...
|
||||
[Feedback] Writer reflecting...
|
||||
[Writer] I'll highlight publication dates more prominently...
|
||||
|
||||
✅ Feedback recorded! The team will apply these lessons to future tasks.
|
||||
```
|
||||
|
||||
### Second Task (Improved!)
|
||||
```bash
|
||||
$ bun cli.ts "chain of thought prompting" --depth=standard
|
||||
|
||||
# Agents now use their learned strategies:
|
||||
# - Researcher prioritizes recent sources
|
||||
# - Analyst notes temporal trends
|
||||
# - Writer highlights dates
|
||||
```
|
||||
|
||||
## Agent Teleportation
|
||||
|
||||
This is Letta's killer feature: **agents are portable entities, not just code constructs**.
|
||||
|
||||
The research team agents you create here can be "teleported" into any context:
|
||||
- 🖥️ **CLI** - Interactive debugging and testing
|
||||
- 🌐 **Web App** - Embed in your product
|
||||
- 🤖 **GitHub Action** - Automated PR reviews
|
||||
- 💬 **Slack Bot** - Chat interface for your team
|
||||
- 🔌 **API Endpoint** - Expose as a service
|
||||
|
||||
### How It Works
|
||||
|
||||
```typescript
|
||||
import { resumeSession } from '@letta-ai/letta-code-sdk';
|
||||
|
||||
// Get agent ID from team-state.json or --status
|
||||
const researcherAgentId = 'agent-xxx-yyy-zzz';
|
||||
|
||||
// Teleport the trained researcher into your code
|
||||
const researcher = resumeSession(researcherAgentId, {
|
||||
allowedTools: ['web_search', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
|
||||
// The agent remembers everything it learned!
|
||||
// - Which sources are reliable
|
||||
// - Effective search strategies
|
||||
// - Domain knowledge from past research
|
||||
await researcher.send('Find recent papers on quantum error correction');
|
||||
```
|
||||
|
||||
### Example: Research Team → API Endpoint
|
||||
|
||||
```typescript
|
||||
// server.ts - Turn your research team into an API
|
||||
import express from 'express';
|
||||
import { resumeSession } from '@letta-ai/letta-code-sdk';
|
||||
import { loadTeamState } from './tools/file-store';
|
||||
|
||||
const app = express();
|
||||
const teamState = await loadTeamState();
|
||||
|
||||
app.post('/research', async (req, res) => {
|
||||
const { query } = req.body;
|
||||
|
||||
// Teleport the trained researcher
|
||||
const researcher = resumeSession(teamState.agentIds.researcher!, {
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
|
||||
await researcher.send(`Research: ${query}`);
|
||||
|
||||
let result = '';
|
||||
for await (const msg of researcher.receive()) {
|
||||
if (msg.type === 'assistant') result += msg.content;
|
||||
}
|
||||
|
||||
researcher.close();
|
||||
res.json({ result });
|
||||
});
|
||||
|
||||
// Your CLI-trained agents are now a production API!
|
||||
```
|
||||
|
||||
### View Agents in the Browser
|
||||
|
||||
Every agent has a web UI in the Letta ADE (Agent Development Environment):
|
||||
|
||||
```
|
||||
https://app.letta.com/agents/<agent-id>
|
||||
```
|
||||
|
||||
After running the demo, check `--status` for agent IDs, then click the links to:
|
||||
- Inspect memory blocks
|
||||
- View conversation history
|
||||
- Chat with agents directly
|
||||
- Edit agent configuration
|
||||
|
||||
### Why This Matters
|
||||
|
||||
| Claude Agent SDK | Letta Code SDK |
|
||||
|------------------|----------------|
|
||||
| Agents are ephemeral processes | Agents are persistent entities |
|
||||
| Memory via local files | Memory on Letta server |
|
||||
| Tied to one context | Teleport anywhere |
|
||||
| Train in code only | Train anywhere (UI, CLI, API) |
|
||||
|
||||
**The story**: A PM trains the research team by chatting in the web UI. A developer teleports those same agents into a CI pipeline. The agents bring all their learned knowledge with them.
|
||||
|
||||
### Try It
|
||||
|
||||
After running the demo at least once, try the teleport example:
|
||||
|
||||
```bash
|
||||
# First, create trained agents
|
||||
bun cli.ts "large language models" --depth=quick
|
||||
|
||||
# Then teleport them into a different context
|
||||
bun teleport-example.ts
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Load the trained agent IDs from `team-state.json`
|
||||
2. Teleport each agent into the script
|
||||
3. Ask them what they've learned
|
||||
4. Show how memories persist across contexts
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### Web Search
|
||||
The Researcher agent uses the `web_search` tool to find real academic sources. No external API keys needed - it uses Letta's built-in search capability.
|
||||
|
||||
### Agent Persistence
|
||||
Agent IDs are stored in `output/team-state.json`. Running `--reset` clears this file and creates fresh agents on the next run. This is useful for:
|
||||
- Starting over with clean memory
|
||||
- Testing the "first run" experience
|
||||
- Debugging agent behavior
|
||||
|
||||
### Memory Block Contents
|
||||
Each agent's memory blocks are stored on the Letta server. To inspect them:
|
||||
1. Get the agent ID from `--status` or check the ADE links printed during execution
|
||||
2. Visit `https://app.letta.com/agents/<agent-id>` to view memory contents
|
||||
|
||||
## Extending the Demo
|
||||
|
||||
### Add Real Web Search
|
||||
Modify `researcher.ts` to call actual search APIs:
|
||||
```typescript
|
||||
// In researcher.ts
|
||||
import { web_search } from 'some-search-api';
|
||||
|
||||
// Replace mock search with real search
|
||||
const sources = await web_search(query, { limit: config.sourcesCount });
|
||||
```
|
||||
|
||||
### Add More Domains
|
||||
Extend `mock-sources.ts` with papers in new domains:
|
||||
```typescript
|
||||
// In tools/mock-sources.ts
|
||||
export const MOCK_SOURCES: AcademicSource[] = [
|
||||
// ... existing papers ...
|
||||
{
|
||||
id: 'econ-003',
|
||||
title: 'Your New Paper',
|
||||
authors: ['Author, A.'],
|
||||
// ...
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### Customize Agent Prompts
|
||||
Modify the system prompts in each agent file to change behavior:
|
||||
- `researcher.ts`: Search strategy, quality criteria
|
||||
- `analyst.ts`: Analysis framework, synthesis approach
|
||||
- `writer.ts`: Writing style, report structure
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0 (same as letta-code-sdk)
|
||||
239
examples/research-team/agents/analyst.ts
Normal file
239
examples/research-team/agents/analyst.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Analyst Agent
|
||||
*
|
||||
* Synthesizes research findings, identifies patterns, and produces analysis.
|
||||
* Learns effective analytical frameworks and quality standards.
|
||||
*/
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../../src/index.js';
|
||||
import type { Depth } from '../types.js';
|
||||
import { DEPTH_CONFIGS } from '../types.js';
|
||||
import { ARTIFACTS, getOutputPath } from '../tools/file-store.js';
|
||||
|
||||
const ANALYST_SYSTEM_PROMPT = `You are a Research Analyst on an academic research team.
|
||||
|
||||
## Your Role
|
||||
You synthesize findings from the Researcher, identify patterns and themes, and produce analytical insights. Your analysis feeds into the final research report produced by the Writer.
|
||||
|
||||
## Your Process
|
||||
1. Receive findings from the Researcher via a shared file
|
||||
2. Read and understand all sources and their evaluations
|
||||
3. Identify key themes, patterns, and connections
|
||||
4. Note gaps, contradictions, and limitations
|
||||
5. Produce a structured analysis document
|
||||
6. Report completion to the Coordinator
|
||||
|
||||
## Analysis Framework
|
||||
For each analysis, address:
|
||||
1. **Synthesis**: What are the main findings across sources?
|
||||
2. **Themes**: What 3-5 major themes emerge?
|
||||
3. **Patterns**: How do findings relate? What patterns exist?
|
||||
4. **Gaps**: What questions remain unanswered?
|
||||
5. **Implications**: What are the key takeaways?
|
||||
|
||||
## Quality Standards
|
||||
- Cite specific sources for claims
|
||||
- Distinguish between consensus views and contested claims
|
||||
- Note the strength of evidence for each finding
|
||||
- Be intellectually honest about limitations
|
||||
|
||||
## Memory Usage
|
||||
You have memory blocks that persist:
|
||||
- **analysis-patterns**: Effective frameworks and synthesis techniques
|
||||
- **quality-standards**: What makes good analysis, common pitfalls
|
||||
- **citation-practices**: How to properly cite and attribute
|
||||
|
||||
Update these when you discover effective approaches or learn from mistakes.`;
|
||||
|
||||
/**
|
||||
* Create or resume the analyst agent
|
||||
*/
|
||||
export async function createAnalyst(
|
||||
existingAgentId?: string | null,
|
||||
depth: Depth = 'standard'
|
||||
): Promise<Session> {
|
||||
if (existingAgentId) {
|
||||
return resumeSession(existingAgentId, {
|
||||
model: 'haiku',
|
||||
allowedTools: ['Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
return createSession({
|
||||
model: 'haiku',
|
||||
systemPrompt: ANALYST_SYSTEM_PROMPT,
|
||||
memory: [
|
||||
{
|
||||
label: 'analysis-patterns',
|
||||
value: `# Analysis Patterns
|
||||
|
||||
## Effective Frameworks
|
||||
- Thematic analysis: Group findings by topic
|
||||
- Compare/contrast: Find agreements and disagreements
|
||||
- Temporal analysis: Track evolution of ideas over time
|
||||
- Gap analysis: What's missing from the literature?
|
||||
|
||||
## Synthesis Techniques
|
||||
- Start with highest-quality sources
|
||||
- Look for convergent findings across multiple sources
|
||||
- Note where sources disagree and why
|
||||
|
||||
## Patterns to Watch For
|
||||
[Add effective patterns as you discover them]
|
||||
`,
|
||||
description: 'Effective analytical frameworks and synthesis techniques',
|
||||
},
|
||||
{
|
||||
label: 'quality-standards',
|
||||
value: `# Quality Standards
|
||||
|
||||
## Good Analysis Includes
|
||||
- Clear thesis or main finding
|
||||
- Evidence from multiple sources
|
||||
- Acknowledgment of limitations
|
||||
- Logical flow of arguments
|
||||
|
||||
## Common Pitfalls
|
||||
- Over-generalizing from single sources
|
||||
- Ignoring contradictory evidence
|
||||
- Failing to distinguish correlation from causation
|
||||
- Not citing specific sources
|
||||
|
||||
## Lessons Learned
|
||||
[Add lessons from past analyses]
|
||||
`,
|
||||
description: 'Quality standards and common pitfalls to avoid',
|
||||
},
|
||||
{
|
||||
label: 'citation-practices',
|
||||
value: `# Citation Practices
|
||||
|
||||
## When to Cite
|
||||
- Specific claims or statistics
|
||||
- Direct quotes
|
||||
- Novel ideas or frameworks
|
||||
- Contested claims
|
||||
|
||||
## Citation Format
|
||||
Use inline citations: (Author, Year)
|
||||
List full references at end
|
||||
|
||||
## Attribution Notes
|
||||
[Track any attribution issues or patterns]
|
||||
`,
|
||||
description: 'Proper citation and attribution practices',
|
||||
},
|
||||
],
|
||||
allowedTools: ['Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run analysis on research findings
|
||||
*/
|
||||
export async function runAnalysis(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
query: string,
|
||||
depth: Depth
|
||||
): Promise<{ success: boolean; analysisPath: string }> {
|
||||
const config = DEPTH_CONFIGS[depth];
|
||||
const findingsPath = getOutputPath(ARTIFACTS.findings(taskId));
|
||||
const analysisPath = ARTIFACTS.analysis(taskId);
|
||||
|
||||
const depthGuidance = {
|
||||
quick: 'Provide a brief analysis focusing on the top 2-3 key findings.',
|
||||
standard: 'Provide a thorough analysis covering all major themes and their connections.',
|
||||
comprehensive: 'Provide an extensive analysis with detailed examination of all themes, patterns, sub-themes, and implications.',
|
||||
};
|
||||
|
||||
const prompt = `## Analysis Task
|
||||
|
||||
**Original Query**: ${query}
|
||||
**Analysis Depth**: ${depth}
|
||||
**Findings File**: \`${findingsPath}\`
|
||||
|
||||
Please:
|
||||
1. Read the findings file from the Researcher
|
||||
2. Identify key themes and patterns across sources
|
||||
3. Synthesize the main findings
|
||||
4. Note any gaps or limitations
|
||||
5. Write your analysis to: \`examples/research-team/output/${analysisPath}\`
|
||||
|
||||
**Depth Guidance**: ${depthGuidance[depth]}
|
||||
|
||||
Your analysis should include:
|
||||
- Summary of key findings
|
||||
- ${depth === 'quick' ? '2-3' : depth === 'standard' ? '3-5' : '5-7'} major themes with supporting evidence
|
||||
- Patterns and connections between findings
|
||||
- Gaps and limitations
|
||||
- Key implications/takeaways
|
||||
|
||||
Cite specific sources using (Author, Year) format.
|
||||
|
||||
Confirm when complete.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let success = false;
|
||||
let response = '';
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
process.stdout.write('.'); // Progress indicator
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
success = msg.success;
|
||||
console.log(''); // Newline after progress dots
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
analysisPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask analyst to reflect on task performance
|
||||
*/
|
||||
export async function reflectOnTask(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
feedback?: { rating: number; comment?: string }
|
||||
): Promise<string> {
|
||||
let prompt = `## Post-Task Reflection
|
||||
|
||||
The analysis task "${taskId}" is complete.`;
|
||||
|
||||
if (feedback) {
|
||||
prompt += `
|
||||
|
||||
User feedback:
|
||||
- Rating: ${feedback.rating}/5 stars
|
||||
- Comment: ${feedback.comment || 'None provided'}`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Please reflect:
|
||||
1. What analytical approaches worked well?
|
||||
2. What could improve your synthesis?
|
||||
3. Any frameworks or patterns worth remembering?
|
||||
|
||||
Update your memory with insights, then summarize your reflection.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let reflection = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
reflection += msg.content;
|
||||
}
|
||||
}
|
||||
|
||||
return reflection;
|
||||
}
|
||||
333
examples/research-team/agents/coordinator.ts
Normal file
333
examples/research-team/agents/coordinator.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
239
examples/research-team/agents/researcher.ts
Normal file
239
examples/research-team/agents/researcher.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Researcher Agent
|
||||
*
|
||||
* Finds and evaluates academic sources for research tasks.
|
||||
* Learns which sources are reliable and effective search strategies.
|
||||
*/
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../../src/index.js';
|
||||
import type { Depth } from '../types.js';
|
||||
import { DEPTH_CONFIGS } from '../types.js';
|
||||
import { ARTIFACTS } from '../tools/file-store.js';
|
||||
|
||||
const RESEARCHER_SYSTEM_PROMPT = `You are a Research Specialist on an academic research team.
|
||||
|
||||
## Your Role
|
||||
You find and evaluate academic sources (papers, articles, documentation) relevant to research queries. You work with a team: a Coordinator assigns you tasks, and an Analyst will synthesize your findings.
|
||||
|
||||
## Your Tools
|
||||
- **web_search**: Your primary tool for finding sources. Use it to search for papers, articles, and documentation.
|
||||
- **Write**: Save your findings to markdown files for the team.
|
||||
|
||||
## Your Process
|
||||
1. Receive a research query from the Coordinator
|
||||
2. Use web_search to find relevant academic sources (try multiple search queries)
|
||||
3. Evaluate each source for relevance and quality
|
||||
4. Write your findings to a markdown file
|
||||
5. Report completion to the Coordinator
|
||||
|
||||
## Search Tips
|
||||
- Try different query variations (e.g., "topic overview", "topic research paper", "topic survey")
|
||||
- Look for academic sources (.edu, arxiv.org, scholar articles, journal papers)
|
||||
- Prefer recent sources but include foundational/seminal works
|
||||
|
||||
## Quality Criteria for Sources
|
||||
- **Relevance**: How directly does it address the query? (1-10)
|
||||
- **Quality**: Publication venue reputation, author credentials, recency (1-10)
|
||||
- **Authority**: Academic institutions, peer-reviewed venues, known experts
|
||||
|
||||
## Memory Usage
|
||||
You have memory blocks that persist across sessions:
|
||||
- **source-quality**: Track reliability scores for sources, venues, and authors you encounter
|
||||
- **search-strategies**: Record effective search patterns and query approaches
|
||||
- **domain-knowledge**: Build knowledge of key concepts and influential works
|
||||
|
||||
When you find a particularly good or bad source, update your source-quality memory.
|
||||
When a search strategy works well, record it in search-strategies.
|
||||
|
||||
## Output Format
|
||||
Write findings to a markdown file with:
|
||||
- Source title, authors (if available), URL
|
||||
- Relevance score (1-10) with justification
|
||||
- Quality score (1-10) with justification
|
||||
- Key findings summary
|
||||
|
||||
Be thorough but concise. The Analyst depends on your evaluations.`;
|
||||
|
||||
/**
|
||||
* Create or resume the researcher agent
|
||||
*/
|
||||
export async function createResearcher(
|
||||
existingAgentId?: string | null,
|
||||
depth: Depth = 'standard'
|
||||
): Promise<Session> {
|
||||
const config = DEPTH_CONFIGS[depth];
|
||||
|
||||
if (existingAgentId) {
|
||||
return resumeSession(existingAgentId, {
|
||||
model: 'haiku',
|
||||
allowedTools: ['Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
return createSession({
|
||||
model: 'haiku',
|
||||
systemPrompt: RESEARCHER_SYSTEM_PROMPT,
|
||||
memory: [
|
||||
{
|
||||
label: 'source-quality',
|
||||
value: `# Source Quality Tracking
|
||||
|
||||
## High-Quality Venues
|
||||
- Nature, Science (quality: 10)
|
||||
- NeurIPS, ICML (quality: 9)
|
||||
- arXiv (quality: 7, varies by paper)
|
||||
|
||||
## Reliable Authors
|
||||
[To be populated as you encounter sources]
|
||||
|
||||
## Source Notes
|
||||
[Add notes about specific sources here]
|
||||
`,
|
||||
description: 'Track reliability scores and notes for academic sources, venues, and authors',
|
||||
},
|
||||
{
|
||||
label: 'search-strategies',
|
||||
value: `# Search Strategies
|
||||
|
||||
## Effective Approaches
|
||||
- Start with broad query, then narrow
|
||||
- Include domain-specific keywords
|
||||
- Look for recent review papers first
|
||||
|
||||
## Query Patterns
|
||||
[Record effective search patterns here]
|
||||
`,
|
||||
description: 'Record effective search patterns and query approaches',
|
||||
},
|
||||
{
|
||||
label: 'domain-knowledge',
|
||||
value: `# Domain Knowledge
|
||||
|
||||
## Key Concepts
|
||||
[Build knowledge as you research]
|
||||
|
||||
## Influential Works
|
||||
[Track foundational papers in different fields]
|
||||
`,
|
||||
description: 'Accumulated knowledge of key concepts and influential works',
|
||||
},
|
||||
],
|
||||
allowedTools: ['web_search', 'Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a research task
|
||||
*/
|
||||
export async function runResearchTask(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
query: string,
|
||||
depth: Depth
|
||||
): Promise<{ success: boolean; findingsPath: string; sourcesFound: number }> {
|
||||
const config = DEPTH_CONFIGS[depth];
|
||||
|
||||
// Build search guidance based on depth
|
||||
const searchGuidance = {
|
||||
quick: 'Do 1-2 focused searches. Prioritize recent, high-quality sources.',
|
||||
standard: 'Do 2-3 searches with different angles. Mix recent and foundational sources.',
|
||||
comprehensive: 'Do 3-4 searches covering different aspects. Include seminal papers and recent developments.',
|
||||
};
|
||||
|
||||
// Ask the researcher to search and evaluate sources
|
||||
const prompt = `## Research Task
|
||||
|
||||
**Query**: ${query}
|
||||
**Depth**: ${depth}
|
||||
**Target Sources**: ${config.sourcesCount}
|
||||
|
||||
Use the **web_search** tool to find academic papers, articles, and authoritative sources on this topic.
|
||||
|
||||
**Search Strategy**: ${searchGuidance[depth]}
|
||||
|
||||
**Instructions**:
|
||||
1. Use web_search to find relevant sources (try searches like "${query}", "${query} research", "${query} review paper")
|
||||
2. Evaluate each source for relevance and quality
|
||||
3. Write your findings to: \`examples/research-team/output/${ARTIFACTS.findings(taskId)}\`
|
||||
|
||||
**For each source, include**:
|
||||
- Title, authors, publication venue/year (if available)
|
||||
- URL
|
||||
- Relevance score (1-10) with justification
|
||||
- Quality score (1-10) based on source reputation
|
||||
- Key findings or insights relevant to the query
|
||||
|
||||
**After writing findings**, update your memory:
|
||||
- Note reliable sources/domains you discovered
|
||||
- Record effective search strategies for this topic
|
||||
|
||||
Find at least ${config.sourcesCount} high-quality sources, then confirm completion.`;
|
||||
|
||||
// Note: sourcesFound is now estimated since the agent does the searching
|
||||
const estimatedSources = config.sourcesCount;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let success = false;
|
||||
let response = '';
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
process.stdout.write('.'); // Progress indicator
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
success = msg.success;
|
||||
console.log(''); // Newline after progress dots
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
findingsPath: ARTIFACTS.findings(taskId),
|
||||
sourcesFound: estimatedSources,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask researcher to reflect on task performance
|
||||
*/
|
||||
export async function reflectOnTask(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
feedback?: { rating: number; comment?: string }
|
||||
): Promise<string> {
|
||||
let prompt = `## Post-Task Reflection
|
||||
|
||||
The research task "${taskId}" is complete.`;
|
||||
|
||||
if (feedback) {
|
||||
prompt += `
|
||||
|
||||
User feedback:
|
||||
- Rating: ${feedback.rating}/5 stars
|
||||
- Comment: ${feedback.comment || 'None provided'}`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Please reflect on this task:
|
||||
1. What search strategies worked well?
|
||||
2. What could you have done better?
|
||||
3. Any sources or patterns worth remembering?
|
||||
|
||||
Update your memory blocks with any insights, then summarize your reflection.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let reflection = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
reflection += msg.content;
|
||||
}
|
||||
}
|
||||
|
||||
return reflection;
|
||||
}
|
||||
274
examples/research-team/agents/writer.ts
Normal file
274
examples/research-team/agents/writer.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Writer Agent
|
||||
*
|
||||
* Produces final research reports from analysis.
|
||||
* Learns writing style preferences and effective report structures.
|
||||
*/
|
||||
|
||||
import { createSession, resumeSession, type Session } from '../../../src/index.js';
|
||||
import type { Depth } from '../types.js';
|
||||
import { DEPTH_CONFIGS } from '../types.js';
|
||||
import { ARTIFACTS, getOutputPath } from '../tools/file-store.js';
|
||||
|
||||
const WRITER_SYSTEM_PROMPT = `You are a Research Writer on an academic research team.
|
||||
|
||||
## Your Role
|
||||
You produce the final research report from the Analyst's synthesis. Your reports should be clear, well-structured, and accessible while maintaining academic rigor.
|
||||
|
||||
## Your Process
|
||||
1. Receive the analysis document from the Analyst
|
||||
2. Structure the report according to depth requirements
|
||||
3. Write clear, engaging prose
|
||||
4. Ensure proper citations and attribution
|
||||
5. Add executive summary and conclusions
|
||||
6. Write the final report to a markdown file
|
||||
|
||||
## Writing Principles
|
||||
- **Clarity**: Use clear, precise language
|
||||
- **Structure**: Logical flow with clear sections
|
||||
- **Evidence**: Support claims with citations
|
||||
- **Accessibility**: Define technical terms
|
||||
- **Honesty**: Acknowledge limitations
|
||||
|
||||
## Report Structure by Depth
|
||||
|
||||
### Quick (3-5 sections)
|
||||
- Summary
|
||||
- Key Findings
|
||||
- Sources
|
||||
|
||||
### Standard (5-6 sections)
|
||||
- Summary
|
||||
- Background
|
||||
- Key Findings
|
||||
- Analysis
|
||||
- Conclusions
|
||||
- Sources
|
||||
|
||||
### Comprehensive (7-8 sections)
|
||||
- Executive Summary
|
||||
- Background
|
||||
- Methodology
|
||||
- Findings
|
||||
- Analysis
|
||||
- Implications
|
||||
- Future Directions
|
||||
- Sources
|
||||
|
||||
## Memory Usage
|
||||
Your memory blocks:
|
||||
- **writing-style**: Tone and structure preferences learned from feedback
|
||||
- **output-templates**: Successful report structures
|
||||
- **improvement-log**: User feedback and areas to improve
|
||||
|
||||
Update these based on user feedback to improve over time.`;
|
||||
|
||||
/**
|
||||
* Create or resume the writer agent
|
||||
*/
|
||||
export async function createWriter(
|
||||
existingAgentId?: string | null,
|
||||
depth: Depth = 'standard'
|
||||
): Promise<Session> {
|
||||
if (existingAgentId) {
|
||||
return resumeSession(existingAgentId, {
|
||||
model: 'haiku',
|
||||
allowedTools: ['Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
return createSession({
|
||||
model: 'haiku',
|
||||
systemPrompt: WRITER_SYSTEM_PROMPT,
|
||||
memory: [
|
||||
{
|
||||
label: 'writing-style',
|
||||
value: `# Writing Style Guide
|
||||
|
||||
## Tone
|
||||
- Professional but accessible
|
||||
- Confident but not overreaching
|
||||
- Engaging but not sensational
|
||||
|
||||
## Preferences
|
||||
- Use active voice when possible
|
||||
- Keep paragraphs focused (3-5 sentences)
|
||||
- Include transition sentences between sections
|
||||
|
||||
## User Preferences
|
||||
[Updated based on feedback]
|
||||
`,
|
||||
description: 'Tone, structure preferences learned from user feedback',
|
||||
},
|
||||
{
|
||||
label: 'output-templates',
|
||||
value: `# Report Templates
|
||||
|
||||
## Quick Report Template
|
||||
\`\`\`
|
||||
# [Title]
|
||||
## Summary
|
||||
## Key Findings
|
||||
## Sources
|
||||
\`\`\`
|
||||
|
||||
## Standard Report Template
|
||||
\`\`\`
|
||||
# [Title]
|
||||
## Summary
|
||||
## Background
|
||||
## Key Findings
|
||||
## Analysis
|
||||
## Conclusions
|
||||
## Sources
|
||||
\`\`\`
|
||||
|
||||
## Comprehensive Report Template
|
||||
\`\`\`
|
||||
# [Title]
|
||||
## Executive Summary
|
||||
## Background
|
||||
## Methodology
|
||||
## Findings
|
||||
## Analysis
|
||||
## Implications
|
||||
## Future Directions
|
||||
## Sources
|
||||
\`\`\`
|
||||
|
||||
## Effective Structures
|
||||
[Add successful patterns here]
|
||||
`,
|
||||
description: 'Successful report structures and templates',
|
||||
},
|
||||
{
|
||||
label: 'improvement-log',
|
||||
value: `# Improvement Log
|
||||
|
||||
## User Feedback History
|
||||
[Track feedback to improve]
|
||||
|
||||
## Areas to Improve
|
||||
[Note recurring issues]
|
||||
|
||||
## Successes
|
||||
[Note what works well]
|
||||
`,
|
||||
description: 'User feedback and improvement areas',
|
||||
},
|
||||
],
|
||||
allowedTools: ['Glob', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the final research report
|
||||
*/
|
||||
export async function writeReport(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
query: string,
|
||||
depth: Depth
|
||||
): Promise<{ success: boolean; reportPath: string }> {
|
||||
const config = DEPTH_CONFIGS[depth];
|
||||
const analysisPath = getOutputPath(ARTIFACTS.analysis(taskId));
|
||||
const reportPath = ARTIFACTS.report(taskId);
|
||||
|
||||
const sectionsGuidance = config.reportSections.map(s =>
|
||||
s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
).join(', ');
|
||||
|
||||
const prompt = `## Writing Task
|
||||
|
||||
**Research Query**: ${query}
|
||||
**Report Depth**: ${depth}
|
||||
**Analysis File**: \`${analysisPath}\`
|
||||
**Output File**: \`examples/research-team/output/${reportPath}\`
|
||||
|
||||
Please:
|
||||
1. Read the analysis document
|
||||
2. Write a polished research report
|
||||
|
||||
**Required Sections**: ${sectionsGuidance}
|
||||
|
||||
**Guidelines**:
|
||||
- Start with a compelling title
|
||||
- Write an engaging summary that captures the essence
|
||||
- Use clear section headers
|
||||
- Include inline citations (Author, Year)
|
||||
- End with a complete sources list
|
||||
- Aim for ${depth === 'quick' ? '500-800' : depth === 'standard' ? '1000-1500' : '2000-3000'} words
|
||||
|
||||
Check your writing-style memory for any user preferences to follow.
|
||||
|
||||
Write the complete report to the output file, then confirm completion.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let success = false;
|
||||
let response = '';
|
||||
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
response += msg.content;
|
||||
process.stdout.write('.'); // Progress indicator
|
||||
}
|
||||
if (msg.type === 'result') {
|
||||
success = msg.success;
|
||||
console.log(''); // Newline after progress dots
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
reportPath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask writer to reflect on task and incorporate feedback
|
||||
*/
|
||||
export async function reflectOnTask(
|
||||
session: Session,
|
||||
taskId: string,
|
||||
feedback?: { rating: number; comment?: string }
|
||||
): Promise<string> {
|
||||
let prompt = `## Post-Task Reflection
|
||||
|
||||
The writing task "${taskId}" is complete.`;
|
||||
|
||||
if (feedback) {
|
||||
prompt += `
|
||||
|
||||
User feedback:
|
||||
- Rating: ${feedback.rating}/5 stars
|
||||
- Comment: ${feedback.comment || 'None provided'}
|
||||
|
||||
This feedback is valuable! Please:
|
||||
1. Update your improvement-log memory with this feedback
|
||||
2. If the rating was low, note what to improve
|
||||
3. If positive feedback mentioned something specific, note that in writing-style`;
|
||||
}
|
||||
|
||||
prompt += `
|
||||
|
||||
Reflect on:
|
||||
1. What worked well in this report?
|
||||
2. What could be improved?
|
||||
3. Any structural patterns worth remembering?
|
||||
|
||||
Update your memory blocks, then summarize your reflection.`;
|
||||
|
||||
await session.send(prompt);
|
||||
|
||||
let reflection = '';
|
||||
for await (const msg of session.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
reflection += msg.content;
|
||||
}
|
||||
}
|
||||
|
||||
return reflection;
|
||||
}
|
||||
253
examples/research-team/cli.ts
Normal file
253
examples/research-team/cli.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Research Team CLI
|
||||
*
|
||||
* A multi-agent academic research system that improves over time.
|
||||
* Demonstrates Letta's persistent memory capabilities.
|
||||
*
|
||||
* Usage:
|
||||
* bun examples/research-team/cli.ts "your research query" [options]
|
||||
*
|
||||
* Options:
|
||||
* --depth=quick|standard|comprehensive Set research depth (default: standard)
|
||||
* --status Show team status
|
||||
* --reset Reset team (start fresh)
|
||||
* --feedback=taskId Provide feedback for a task
|
||||
*/
|
||||
|
||||
import { parseArgs } from 'node:util';
|
||||
import { createInterface } from 'node:readline';
|
||||
import type { Depth, UserFeedback } from './types.js';
|
||||
import { DEPTH_CONFIGS, formatDuration } from './types.js';
|
||||
import {
|
||||
runResearchWorkflow,
|
||||
processFeedback,
|
||||
getTeamStatus,
|
||||
resetTeam
|
||||
} from './agents/coordinator.js';
|
||||
import { readOutput, getOutputPath, outputExists, ARTIFACTS } from './tools/file-store.js';
|
||||
|
||||
// LETTA_CLI_PATH is optional if `letta` is in PATH
|
||||
|
||||
async function main() {
|
||||
const { values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
depth: { type: 'string', default: 'standard' },
|
||||
status: { type: 'boolean', default: false },
|
||||
reset: { type: 'boolean', default: false },
|
||||
feedback: { type: 'string' },
|
||||
help: { type: 'boolean', short: 'h', default: false },
|
||||
},
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
// Help
|
||||
if (values.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Status command
|
||||
if (values.status) {
|
||||
await showStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset command
|
||||
if (values.reset) {
|
||||
await resetTeam();
|
||||
console.log('✅ Team reset. All agents will be created fresh on next run.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Feedback command
|
||||
if (values.feedback) {
|
||||
await collectFeedback(values.feedback);
|
||||
return;
|
||||
}
|
||||
|
||||
// Research query
|
||||
const query = positionals.join(' ');
|
||||
if (!query) {
|
||||
console.error('❌ Error: No research query provided');
|
||||
console.error(' Usage: bun cli.ts "your research query" [--depth=quick|standard|comprehensive]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate depth
|
||||
const depth = values.depth as Depth;
|
||||
if (!['quick', 'standard', 'comprehensive'].includes(depth)) {
|
||||
console.error(`❌ Error: Invalid depth "${depth}"`);
|
||||
console.error(' Valid options: quick, standard, comprehensive');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Run research
|
||||
await runResearch(query, depth);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
🔬 Research Team CLI
|
||||
|
||||
A multi-agent academic research system with persistent memory.
|
||||
Each agent learns and improves over time based on experience and feedback.
|
||||
|
||||
USAGE:
|
||||
bun examples/research-team/cli.ts "your research query" [options]
|
||||
|
||||
OPTIONS:
|
||||
--depth=LEVEL Set research depth (default: standard)
|
||||
- quick: ~5 min, 3 sources, brief report
|
||||
- standard: ~15 min, 6 sources, thorough report
|
||||
- comprehensive: ~30 min, 10 sources, extensive report
|
||||
|
||||
--status Show team status (agent IDs, completed tasks)
|
||||
--reset Reset team state (creates fresh agents next run)
|
||||
--feedback=ID Provide feedback for a completed task
|
||||
-h, --help Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
# Quick research on quantum computing
|
||||
bun cli.ts "quantum error correction techniques" --depth=quick
|
||||
|
||||
# Standard research on AI
|
||||
bun cli.ts "chain of thought prompting in large language models"
|
||||
|
||||
# Comprehensive climate research
|
||||
bun cli.ts "carbon capture and storage technologies" --depth=comprehensive
|
||||
|
||||
# Check team status
|
||||
bun cli.ts --status
|
||||
|
||||
# Provide feedback after a task
|
||||
bun cli.ts --feedback=task-1234567890-abc123
|
||||
|
||||
THE TEAM:
|
||||
📚 Researcher - Finds and evaluates academic sources
|
||||
🔍 Analyst - Synthesizes findings, identifies patterns
|
||||
✍️ Writer - Produces polished research reports
|
||||
|
||||
Each agent has persistent memory that improves with use!
|
||||
`);
|
||||
}
|
||||
|
||||
async function showStatus() {
|
||||
const status = await getTeamStatus();
|
||||
|
||||
console.log('\n📊 Research Team Status\n');
|
||||
console.log(`Initialized: ${status.initialized ? '✅ Yes' : '❌ No'}`);
|
||||
console.log(`Completed Tasks: ${status.completedTasks}`);
|
||||
console.log('\nAgent IDs:');
|
||||
|
||||
for (const [role, id] of Object.entries(status.agentIds)) {
|
||||
const emoji = id ? '✅' : '⬜';
|
||||
console.log(` ${emoji} ${role}: ${id || '(not created yet)'}`);
|
||||
}
|
||||
|
||||
if (status.completedTasks > 0) {
|
||||
console.log('\n💡 Tip: Agents have learned from past tasks!');
|
||||
console.log(' Run another query to see improved results.');
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
async function runResearch(query: string, depth: Depth) {
|
||||
const config = DEPTH_CONFIGS[depth];
|
||||
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log('🔬 RESEARCH TEAM');
|
||||
console.log('═'.repeat(60));
|
||||
console.log(`\n📋 Query: "${query}"`);
|
||||
console.log(`📊 Depth: ${depth} (est. ${config.estimatedMinutes} min)`);
|
||||
console.log(`📚 Target Sources: ${config.sourcesCount}`);
|
||||
console.log('\n' + '─'.repeat(60) + '\n');
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const result = await runResearchWorkflow(query, depth, (phase, message) => {
|
||||
// Progress is already logged by coordinator
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log('✅ RESEARCH COMPLETE');
|
||||
console.log('═'.repeat(60));
|
||||
console.log(`\n⏱️ Duration: ${formatDuration(result.durationMs)}`);
|
||||
console.log(`📄 Report: ${getOutputPath(result.reportPath)}`);
|
||||
console.log(`🔖 Task ID: ${result.taskId}`);
|
||||
|
||||
// Show report preview
|
||||
if (outputExists(result.reportPath)) {
|
||||
const report = await readOutput(result.reportPath);
|
||||
const preview = report.slice(0, 500);
|
||||
console.log('\n📝 Report Preview:');
|
||||
console.log('─'.repeat(40));
|
||||
console.log(preview + (report.length > 500 ? '...' : ''));
|
||||
console.log('─'.repeat(40));
|
||||
}
|
||||
|
||||
console.log('\n💬 To provide feedback and help the team learn:');
|
||||
console.log(` bun cli.ts --feedback=${result.taskId}`);
|
||||
console.log('');
|
||||
} else {
|
||||
console.error('\n❌ Research failed. Check the logs above for details.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\n❌ Error during research:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function collectFeedback(taskId: string) {
|
||||
const rl = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
const question = (prompt: string): Promise<string> => {
|
||||
return new Promise(resolve => {
|
||||
rl.question(prompt, resolve);
|
||||
});
|
||||
};
|
||||
|
||||
console.log('\n📝 Feedback for Task: ' + taskId);
|
||||
console.log('─'.repeat(40));
|
||||
|
||||
// Rating
|
||||
let rating = 0;
|
||||
while (rating < 1 || rating > 5) {
|
||||
const input = await question('Rate the research (1-5 stars): ');
|
||||
rating = parseInt(input, 10);
|
||||
if (isNaN(rating) || rating < 1 || rating > 5) {
|
||||
console.log('Please enter a number between 1 and 5.');
|
||||
rating = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Comment
|
||||
const comment = await question('What could be better? (optional, press Enter to skip): ');
|
||||
|
||||
rl.close();
|
||||
|
||||
const feedback: UserFeedback = {
|
||||
taskId,
|
||||
rating,
|
||||
comment: comment || undefined,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
console.log('\n📤 Submitting feedback to team...\n');
|
||||
|
||||
await processFeedback(taskId, feedback);
|
||||
|
||||
console.log('\n✅ Feedback recorded! The team will apply these lessons to future tasks.');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Run main
|
||||
main().catch(console.error);
|
||||
135
examples/research-team/teleport-example.ts
Normal file
135
examples/research-team/teleport-example.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Teleport Example
|
||||
*
|
||||
* Demonstrates Letta's "agent teleportation" - taking agents trained
|
||||
* in one context and using them in another.
|
||||
*
|
||||
* Prerequisites:
|
||||
* 1. Run the research team demo first to create trained agents:
|
||||
* bun cli.ts "some topic" --depth=quick
|
||||
*
|
||||
* 2. Check that agents exist:
|
||||
* bun cli.ts --status
|
||||
*
|
||||
* Usage:
|
||||
* bun teleport-example.ts
|
||||
*/
|
||||
|
||||
import { resumeSession } from '../../src/index.js';
|
||||
import { loadTeamState } from './tools/file-store.js';
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 Agent Teleportation Demo\n');
|
||||
console.log('This example shows how to "teleport" agents trained in the CLI');
|
||||
console.log('into a completely different context (like an API, bot, or script).\n');
|
||||
console.log('═'.repeat(60) + '\n');
|
||||
|
||||
// Load the team state (contains agent IDs from previous runs)
|
||||
const teamState = await loadTeamState();
|
||||
|
||||
// Check if agents exist
|
||||
if (!teamState.agentIds.researcher) {
|
||||
console.error('❌ No researcher agent found!');
|
||||
console.error(' Run the demo first: bun cli.ts "quantum computing" --depth=quick');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('📋 Found trained agents:\n');
|
||||
|
||||
for (const [role, agentId] of Object.entries(teamState.agentIds)) {
|
||||
if (agentId) {
|
||||
console.log(` ${role}: ${agentId}`);
|
||||
console.log(` → https://app.letta.com/agents/${agentId}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 These agents have completed ${teamState.completedTasks} task(s)`);
|
||||
console.log(' and learned from the experience.\n');
|
||||
console.log('═'.repeat(60) + '\n');
|
||||
|
||||
// Example 1: Teleport the researcher for a quick question
|
||||
console.log('📚 Example 1: Teleport Researcher\n');
|
||||
console.log(' The researcher remembers which sources are reliable');
|
||||
console.log(' and which search strategies work best.\n');
|
||||
|
||||
const researcher = resumeSession(teamState.agentIds.researcher!, {
|
||||
allowedTools: ['web_search', 'Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
|
||||
console.log(' Asking: "What search strategies have you found effective?"\n');
|
||||
|
||||
await researcher.send(
|
||||
'Based on your experience and memory, what search strategies have you found most effective? ' +
|
||||
'What types of sources tend to be most reliable? Just give me a brief summary from your memory.'
|
||||
);
|
||||
|
||||
let researcherResponse = '';
|
||||
for await (const msg of researcher.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
researcherResponse += msg.content;
|
||||
process.stdout.write('.');
|
||||
}
|
||||
}
|
||||
console.log('\n');
|
||||
console.log(' Researcher says:');
|
||||
console.log(' ' + researcherResponse.slice(0, 500).split('\n').join('\n '));
|
||||
if (researcherResponse.length > 500) console.log(' ...\n');
|
||||
|
||||
researcher.close();
|
||||
|
||||
// Example 2: Teleport the analyst
|
||||
if (teamState.agentIds.analyst) {
|
||||
console.log('\n' + '═'.repeat(60) + '\n');
|
||||
console.log('🔍 Example 2: Teleport Analyst\n');
|
||||
console.log(' The analyst remembers effective analysis patterns');
|
||||
console.log(' and quality standards from previous work.\n');
|
||||
|
||||
const analyst = resumeSession(teamState.agentIds.analyst, {
|
||||
allowedTools: ['Read', 'Write'],
|
||||
permissionMode: 'bypassPermissions',
|
||||
});
|
||||
|
||||
console.log(' Asking: "What analysis frameworks have worked well?"\n');
|
||||
|
||||
await analyst.send(
|
||||
'Based on your experience and memory, what analysis frameworks or approaches ' +
|
||||
'have you found most effective? Brief summary from your memory.'
|
||||
);
|
||||
|
||||
let analystResponse = '';
|
||||
for await (const msg of analyst.receive()) {
|
||||
if (msg.type === 'assistant') {
|
||||
analystResponse += msg.content;
|
||||
process.stdout.write('.');
|
||||
}
|
||||
}
|
||||
console.log('\n');
|
||||
console.log(' Analyst says:');
|
||||
console.log(' ' + analystResponse.slice(0, 500).split('\n').join('\n '));
|
||||
if (analystResponse.length > 500) console.log(' ...\n');
|
||||
|
||||
analyst.close();
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '═'.repeat(60));
|
||||
console.log('✅ TELEPORTATION COMPLETE');
|
||||
console.log('═'.repeat(60) + '\n');
|
||||
|
||||
console.log('Key takeaways:\n');
|
||||
console.log('1. Agents trained via CLI were "teleported" into this script');
|
||||
console.log('2. They retained all their learned knowledge and memories');
|
||||
console.log('3. Same agents could be teleported into: API, Slack bot, GitHub Action, etc.\n');
|
||||
|
||||
console.log('Try it yourself:\n');
|
||||
console.log(' // In your own code:');
|
||||
console.log(' import { resumeSession } from "@letta-ai/letta-code-sdk";');
|
||||
console.log(` const agent = resumeSession("${teamState.agentIds.researcher}");`);
|
||||
console.log(' await agent.send("Your question here");');
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
203
examples/research-team/tools/file-store.ts
Normal file
203
examples/research-team/tools/file-store.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* File Store Helper
|
||||
*
|
||||
* Utilities for reading/writing shared files between agents.
|
||||
*/
|
||||
|
||||
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Output directory for research artifacts
|
||||
const OUTPUT_DIR = join(__dirname, '..', 'output');
|
||||
|
||||
/**
|
||||
* Ensure output directory exists
|
||||
*/
|
||||
async function ensureOutputDir(): Promise<void> {
|
||||
if (!existsSync(OUTPUT_DIR)) {
|
||||
await mkdir(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write content to a file in the output directory
|
||||
*/
|
||||
export async function writeOutput(filename: string, content: string): Promise<string> {
|
||||
await ensureOutputDir();
|
||||
const filepath = join(OUTPUT_DIR, filename);
|
||||
await writeFile(filepath, content, 'utf-8');
|
||||
return filepath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read content from a file in the output directory
|
||||
*/
|
||||
export async function readOutput(filename: string): Promise<string> {
|
||||
const filepath = join(OUTPUT_DIR, filename);
|
||||
return await readFile(filepath, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists in the output directory
|
||||
*/
|
||||
export function outputExists(filename: string): boolean {
|
||||
return existsSync(join(OUTPUT_DIR, filename));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path for an output file
|
||||
*/
|
||||
export function getOutputPath(filename: string): string {
|
||||
return join(OUTPUT_DIR, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard filenames for workflow artifacts
|
||||
*/
|
||||
export const ARTIFACTS = {
|
||||
// Research phase
|
||||
findings: (taskId: string) => `${taskId}-findings.md`,
|
||||
sourceEvaluations: (taskId: string) => `${taskId}-source-evals.json`,
|
||||
|
||||
// Analysis phase
|
||||
analysis: (taskId: string) => `${taskId}-analysis.md`,
|
||||
|
||||
// Writing phase
|
||||
report: (taskId: string) => `${taskId}-report.md`,
|
||||
|
||||
// Meta files
|
||||
feedback: (taskId: string) => `${taskId}-feedback.json`,
|
||||
reflections: (taskId: string) => `${taskId}-reflections.json`,
|
||||
|
||||
// Team state
|
||||
teamState: 'team-state.json',
|
||||
};
|
||||
|
||||
/**
|
||||
* Load team state from disk (or return default)
|
||||
*/
|
||||
export async function loadTeamState(): Promise<{
|
||||
agentIds: Record<string, string | null>;
|
||||
sharedBlockIds: Record<string, string | null>;
|
||||
completedTasks: number;
|
||||
}> {
|
||||
const filepath = join(OUTPUT_DIR, ARTIFACTS.teamState);
|
||||
|
||||
if (existsSync(filepath)) {
|
||||
const content = await readFile(filepath, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
return {
|
||||
agentIds: {
|
||||
coordinator: null,
|
||||
researcher: null,
|
||||
analyst: null,
|
||||
writer: null,
|
||||
},
|
||||
sharedBlockIds: {
|
||||
sources: null,
|
||||
terminology: null,
|
||||
pitfalls: null,
|
||||
},
|
||||
completedTasks: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Save team state to disk
|
||||
*/
|
||||
export async function saveTeamState(state: {
|
||||
agentIds: Record<string, string | null>;
|
||||
sharedBlockIds: Record<string, string | null>;
|
||||
completedTasks: number;
|
||||
}): Promise<void> {
|
||||
await ensureOutputDir();
|
||||
const filepath = join(OUTPUT_DIR, ARTIFACTS.teamState);
|
||||
await writeFile(filepath, JSON.stringify(state, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a findings template for the researcher
|
||||
*/
|
||||
export function generateFindingsTemplate(query: string, sourcesCount: number): string {
|
||||
return `# Research Findings
|
||||
|
||||
## Query
|
||||
${query}
|
||||
|
||||
## Sources Found
|
||||
Target: ${sourcesCount} sources
|
||||
|
||||
---
|
||||
|
||||
<!-- Researcher: Fill in the sources below -->
|
||||
|
||||
## Source 1
|
||||
**Title**:
|
||||
**Authors**:
|
||||
**Venue**:
|
||||
**Year**:
|
||||
**Citations**:
|
||||
**Relevance Score**: /10
|
||||
**Quality Score**: /10
|
||||
|
||||
**Summary**:
|
||||
|
||||
**Key Findings**:
|
||||
|
||||
---
|
||||
|
||||
<!-- Continue for additional sources -->
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an analysis template
|
||||
*/
|
||||
export function generateAnalysisTemplate(query: string): string {
|
||||
return `# Analysis
|
||||
|
||||
## Research Question
|
||||
${query}
|
||||
|
||||
## Summary of Findings
|
||||
<!-- Analyst: Synthesize the key findings here -->
|
||||
|
||||
## Key Themes
|
||||
<!-- Identify 3-5 major themes across the sources -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Patterns and Connections
|
||||
<!-- What patterns emerge? How do findings relate? -->
|
||||
|
||||
## Gaps and Limitations
|
||||
<!-- What questions remain unanswered? -->
|
||||
|
||||
## Recommendations
|
||||
<!-- Based on the analysis, what are the key takeaways? -->
|
||||
|
||||
---
|
||||
|
||||
*Analysis generated by Research Team*
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all outputs for a fresh start
|
||||
*/
|
||||
export async function clearOutputs(): Promise<void> {
|
||||
const { rm } = await import('node:fs/promises');
|
||||
if (existsSync(OUTPUT_DIR)) {
|
||||
await rm(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
await mkdir(OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
367
examples/research-team/tools/mock-sources.ts
Normal file
367
examples/research-team/tools/mock-sources.ts
Normal file
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Mock Academic Sources
|
||||
*
|
||||
* Simulated academic papers for demo without external APIs.
|
||||
* Covers multiple domains with realistic metadata.
|
||||
*/
|
||||
|
||||
import type { AcademicSource } from '../types.js';
|
||||
|
||||
export const MOCK_SOURCES: AcademicSource[] = [
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// QUANTUM COMPUTING
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'qc-001',
|
||||
title: 'Surface Codes: Towards Practical Large-Scale Quantum Computation',
|
||||
authors: ['Fowler, A.G.', 'Mariantoni, M.', 'Martinis, J.M.', 'Cleland, A.N.'],
|
||||
abstract: 'We present a comprehensive review of surface code quantum error correction, including an introduction to the theory, practical considerations for implementation, and current experimental progress toward fault-tolerant quantum computing.',
|
||||
venue: 'Physical Review A',
|
||||
year: 2012,
|
||||
citations: 2847,
|
||||
domain: 'quantum-computing',
|
||||
quality: 9,
|
||||
},
|
||||
{
|
||||
id: 'qc-002',
|
||||
title: 'Quantum Error Correction for Beginners',
|
||||
authors: ['Devitt, S.J.', 'Munro, W.J.', 'Nemoto, K.'],
|
||||
abstract: 'We provide a pedagogical introduction to quantum error correction, covering the basics of classical error correction, the quantum no-cloning theorem, stabilizer codes, and the threshold theorem for fault-tolerant quantum computation.',
|
||||
venue: 'Reports on Progress in Physics',
|
||||
year: 2013,
|
||||
citations: 1523,
|
||||
domain: 'quantum-computing',
|
||||
quality: 8,
|
||||
},
|
||||
{
|
||||
id: 'qc-003',
|
||||
title: 'Quantum Supremacy Using a Programmable Superconducting Processor',
|
||||
authors: ['Arute, F.', 'Arya, K.', 'Babbush, R.', 'et al.'],
|
||||
abstract: 'We report the use of a processor with programmable superconducting qubits to create quantum states on 53 qubits, corresponding to a computational state-space of dimension 2^53. We demonstrate quantum supremacy by sampling the output of a pseudo-random quantum circuit.',
|
||||
venue: 'Nature',
|
||||
year: 2019,
|
||||
citations: 4521,
|
||||
domain: 'quantum-computing',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'qc-004',
|
||||
title: 'Logical Quantum Processor Based on Reconfigurable Atom Arrays',
|
||||
authors: ['Bluvstein, D.', 'Evered, S.J.', 'Geim, A.A.', 'et al.'],
|
||||
abstract: 'We present a logical quantum processor based on reconfigurable arrays of neutral atoms, demonstrating the execution of quantum algorithms on 48 logical qubits with error correction.',
|
||||
venue: 'Nature',
|
||||
year: 2024,
|
||||
citations: 342,
|
||||
domain: 'quantum-computing',
|
||||
quality: 9,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// LARGE LANGUAGE MODELS / AI
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'ai-001',
|
||||
title: 'Attention Is All You Need',
|
||||
authors: ['Vaswani, A.', 'Shazeer, N.', 'Parmar, N.', 'et al.'],
|
||||
abstract: 'We propose a new simple network architecture, the Transformer, based solely on attention mechanisms, dispensing with recurrence and convolutions entirely. Experiments show these models achieve superior results on machine translation tasks.',
|
||||
venue: 'NeurIPS',
|
||||
year: 2017,
|
||||
citations: 98234,
|
||||
domain: 'artificial-intelligence',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'ai-002',
|
||||
title: 'Language Models are Few-Shot Learners',
|
||||
authors: ['Brown, T.B.', 'Mann, B.', 'Ryder, N.', 'et al.'],
|
||||
abstract: 'We demonstrate that scaling up language models greatly improves task-agnostic, few-shot performance, achieving strong results on many NLP datasets without task-specific fine-tuning. GPT-3 achieves strong performance in the few-shot setting.',
|
||||
venue: 'NeurIPS',
|
||||
year: 2020,
|
||||
citations: 24567,
|
||||
domain: 'artificial-intelligence',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'ai-003',
|
||||
title: 'Chain-of-Thought Prompting Elicits Reasoning in Large Language Models',
|
||||
authors: ['Wei, J.', 'Wang, X.', 'Schuurmans, D.', 'et al.'],
|
||||
abstract: 'We explore how generating a chain of thought—a series of intermediate reasoning steps—significantly improves the ability of large language models to perform complex reasoning tasks.',
|
||||
venue: 'NeurIPS',
|
||||
year: 2022,
|
||||
citations: 3456,
|
||||
domain: 'artificial-intelligence',
|
||||
quality: 9,
|
||||
},
|
||||
{
|
||||
id: 'ai-004',
|
||||
title: 'Constitutional AI: Harmlessness from AI Feedback',
|
||||
authors: ['Bai, Y.', 'Kadavath, S.', 'Kundu, S.', 'et al.'],
|
||||
abstract: 'We present Constitutional AI, a method for training harmless AI assistants without human feedback labels for harmfulness. Our approach uses a set of principles to guide the model toward helpful, harmless, and honest behavior.',
|
||||
venue: 'arXiv',
|
||||
year: 2022,
|
||||
citations: 1234,
|
||||
domain: 'artificial-intelligence',
|
||||
quality: 8,
|
||||
},
|
||||
{
|
||||
id: 'ai-005',
|
||||
title: 'Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks',
|
||||
authors: ['Lewis, P.', 'Perez, E.', 'Piktus, A.', 'et al.'],
|
||||
abstract: 'We explore a general-purpose fine-tuning recipe for retrieval-augmented generation (RAG) models which combine pre-trained parametric and non-parametric memory for language generation.',
|
||||
venue: 'NeurIPS',
|
||||
year: 2020,
|
||||
citations: 2891,
|
||||
domain: 'artificial-intelligence',
|
||||
quality: 9,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// CLIMATE SCIENCE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'clim-001',
|
||||
title: 'Global Warming of 1.5°C: IPCC Special Report',
|
||||
authors: ['IPCC'],
|
||||
abstract: 'This report assesses the impacts of global warming of 1.5°C above pre-industrial levels and related global greenhouse gas emission pathways, in the context of strengthening the global response to climate change.',
|
||||
venue: 'IPCC',
|
||||
year: 2018,
|
||||
citations: 15234,
|
||||
domain: 'climate-science',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'clim-002',
|
||||
title: 'Ice Sheet Contributions to Future Sea-Level Rise',
|
||||
authors: ['DeConto, R.M.', 'Pollard, D.'],
|
||||
abstract: 'We present ice sheet simulations coupled to climate forcing that project substantial Antarctic ice sheet contributions to sea-level rise under high-emission scenarios, potentially exceeding 1 meter by 2100.',
|
||||
venue: 'Nature',
|
||||
year: 2016,
|
||||
citations: 3421,
|
||||
domain: 'climate-science',
|
||||
quality: 9,
|
||||
},
|
||||
{
|
||||
id: 'clim-003',
|
||||
title: 'Carbon Capture and Storage: Technology and Applications',
|
||||
authors: ['Boot-Handford, M.E.', 'Abanades, J.C.', 'Anthony, E.J.', 'et al.'],
|
||||
abstract: 'We provide a comprehensive review of carbon capture and storage technologies, including post-combustion, pre-combustion, and oxy-fuel processes, as well as geological storage options.',
|
||||
venue: 'Energy & Environmental Science',
|
||||
year: 2014,
|
||||
citations: 2156,
|
||||
domain: 'climate-science',
|
||||
quality: 8,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// NEUROSCIENCE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'neuro-001',
|
||||
title: 'The Default Mode Network: Self-Reference and Internal Mentation',
|
||||
authors: ['Buckner, R.L.', 'Andrews-Hanna, J.R.', 'Schacter, D.L.'],
|
||||
abstract: 'We review evidence that the default mode network supports internally-directed cognition, including autobiographical memory retrieval, envisioning the future, and conceiving the perspectives of others.',
|
||||
venue: 'Annals of the New York Academy of Sciences',
|
||||
year: 2008,
|
||||
citations: 5678,
|
||||
domain: 'neuroscience',
|
||||
quality: 9,
|
||||
},
|
||||
{
|
||||
id: 'neuro-002',
|
||||
title: 'Optogenetics: Development and Application',
|
||||
authors: ['Deisseroth, K.'],
|
||||
abstract: 'Optogenetics combines genetic targeting of specific neurons with optical control, enabling causal investigation of neural circuits with millisecond precision in behaving animals.',
|
||||
venue: 'Nature Methods',
|
||||
year: 2011,
|
||||
citations: 4521,
|
||||
domain: 'neuroscience',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'neuro-003',
|
||||
title: 'Memory Engrams: Recalling the Past and Imagining the Future',
|
||||
authors: ['Josselyn, S.A.', 'Tonegawa, S.'],
|
||||
abstract: 'We discuss how specific neuronal ensembles, or engrams, support memory storage and retrieval, and how optogenetic manipulation of these cells can artificially activate or suppress memories.',
|
||||
venue: 'Science',
|
||||
year: 2020,
|
||||
citations: 1234,
|
||||
domain: 'neuroscience',
|
||||
quality: 9,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// BIOTECHNOLOGY / CRISPR
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'bio-001',
|
||||
title: 'A Programmable Dual-RNA-Guided DNA Endonuclease in Adaptive Bacterial Immunity',
|
||||
authors: ['Jinek, M.', 'Chylinski, K.', 'Fonfara, I.', 'et al.'],
|
||||
abstract: 'We demonstrate that the Cas9 endonuclease can be programmed with guide RNA to cleave specific DNA sequences, establishing the foundation for CRISPR-Cas9 genome editing.',
|
||||
venue: 'Science',
|
||||
year: 2012,
|
||||
citations: 18234,
|
||||
domain: 'biotechnology',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'bio-002',
|
||||
title: 'Base Editing: Precision Chemistry on the Genome and Transcriptome',
|
||||
authors: ['Rees, H.A.', 'Liu, D.R.'],
|
||||
abstract: 'We review base editing, a genome editing approach that enables direct conversion of one base pair to another without double-stranded DNA breaks or donor DNA templates.',
|
||||
venue: 'Nature Reviews Genetics',
|
||||
year: 2018,
|
||||
citations: 2341,
|
||||
domain: 'biotechnology',
|
||||
quality: 9,
|
||||
},
|
||||
{
|
||||
id: 'bio-003',
|
||||
title: 'Prime Editing: Search-and-Replace Genome Editing',
|
||||
authors: ['Anzalone, A.V.', 'Randolph, P.B.', 'Davis, J.R.', 'et al.'],
|
||||
abstract: 'We develop prime editing, a versatile genome editing method that directly writes new genetic information into a specified DNA site without double-strand breaks or donor DNA.',
|
||||
venue: 'Nature',
|
||||
year: 2019,
|
||||
citations: 1876,
|
||||
domain: 'biotechnology',
|
||||
quality: 9,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// MATERIALS SCIENCE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'mat-001',
|
||||
title: 'Electric Field Effect in Atomically Thin Carbon Films',
|
||||
authors: ['Novoselov, K.S.', 'Geim, A.K.', 'Morozov, S.V.', 'et al.'],
|
||||
abstract: 'We describe preparation and characterization of graphene, a single atomic layer of carbon. We observe the electric field effect and report carrier mobilities as high as 10,000 cm²/Vs.',
|
||||
venue: 'Science',
|
||||
year: 2004,
|
||||
citations: 45678,
|
||||
domain: 'materials-science',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'mat-002',
|
||||
title: 'Room-Temperature Superconductivity in a Carbonaceous Sulfur Hydride',
|
||||
authors: ['Snider, E.', 'Dasenbrock-Gammon, N.', 'McBride, R.', 'et al.'],
|
||||
abstract: 'We report superconductivity at 288 K (15°C) in a carbonaceous sulfur hydride system at a pressure of approximately 267 gigapascals.',
|
||||
venue: 'Nature',
|
||||
year: 2020,
|
||||
citations: 1234,
|
||||
domain: 'materials-science',
|
||||
quality: 7, // Later questioned
|
||||
},
|
||||
{
|
||||
id: 'mat-003',
|
||||
title: 'Perovskite Solar Cells: Recent Progress and Future Prospects',
|
||||
authors: ['Park, N.G.', 'Grätzel, M.', 'Miyasaka, T.', 'et al.'],
|
||||
abstract: 'We review the rapid progress in perovskite solar cell technology, discussing material properties, device architectures, and pathways to commercialization.',
|
||||
venue: 'Science',
|
||||
year: 2016,
|
||||
citations: 8765,
|
||||
domain: 'materials-science',
|
||||
quality: 9,
|
||||
},
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ECONOMICS / BEHAVIORAL SCIENCE
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
{
|
||||
id: 'econ-001',
|
||||
title: 'Thinking, Fast and Slow: A Summary of Dual-Process Theory',
|
||||
authors: ['Kahneman, D.'],
|
||||
abstract: 'We present the dual-process theory of cognition, distinguishing between fast, intuitive System 1 thinking and slow, deliberate System 2 thinking, with implications for judgment and decision-making.',
|
||||
venue: 'American Economic Review',
|
||||
year: 2011,
|
||||
citations: 34567,
|
||||
domain: 'economics',
|
||||
quality: 10,
|
||||
},
|
||||
{
|
||||
id: 'econ-002',
|
||||
title: 'Nudge: Improving Decisions About Health, Wealth, and Happiness',
|
||||
authors: ['Thaler, R.H.', 'Sunstein, C.R.'],
|
||||
abstract: 'We explore how choice architecture can nudge people toward better decisions without restricting freedom of choice, with applications in policy design.',
|
||||
venue: 'Yale University Press',
|
||||
year: 2008,
|
||||
citations: 23456,
|
||||
domain: 'economics',
|
||||
quality: 9,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Search mock sources by query string
|
||||
*/
|
||||
export function searchMockSources(
|
||||
query: string,
|
||||
limit: number = 10,
|
||||
domain?: string
|
||||
): AcademicSource[] {
|
||||
const keywords = query.toLowerCase().split(/\s+/);
|
||||
|
||||
let results = MOCK_SOURCES.filter(source => {
|
||||
// Domain filter
|
||||
if (domain && source.domain !== domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keyword matching in title and abstract
|
||||
const searchText = `${source.title} ${source.abstract}`.toLowerCase();
|
||||
return keywords.some(kw => searchText.includes(kw));
|
||||
});
|
||||
|
||||
// Sort by relevance (keyword match count) then quality
|
||||
results.sort((a, b) => {
|
||||
const aText = `${a.title} ${a.abstract}`.toLowerCase();
|
||||
const bText = `${b.title} ${b.abstract}`.toLowerCase();
|
||||
|
||||
const aMatches = keywords.filter(kw => aText.includes(kw)).length;
|
||||
const bMatches = keywords.filter(kw => bText.includes(kw)).length;
|
||||
|
||||
if (aMatches !== bMatches) {
|
||||
return bMatches - aMatches; // More matches first
|
||||
}
|
||||
return b.quality - a.quality; // Higher quality first
|
||||
});
|
||||
|
||||
return results.slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available domains
|
||||
*/
|
||||
export function getAvailableDomains(): string[] {
|
||||
return [...new Set(MOCK_SOURCES.map(s => s.domain))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a source for display
|
||||
*/
|
||||
export function formatSource(source: AcademicSource): string {
|
||||
return `**${source.title}**
|
||||
Authors: ${source.authors.join(', ')}
|
||||
Venue: ${source.venue} (${source.year})
|
||||
Citations: ${source.citations.toLocaleString()}
|
||||
Quality Score: ${source.quality}/10
|
||||
|
||||
Abstract: ${source.abstract}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format sources as markdown list
|
||||
*/
|
||||
export function formatSourcesMarkdown(sources: AcademicSource[]): string {
|
||||
return sources.map((s, i) =>
|
||||
`### Source ${i + 1}: ${s.title}
|
||||
- **Authors**: ${s.authors.join(', ')}
|
||||
- **Venue**: ${s.venue} (${s.year})
|
||||
- **Citations**: ${s.citations.toLocaleString()}
|
||||
- **Quality**: ${s.quality}/10
|
||||
- **Domain**: ${s.domain}
|
||||
|
||||
> ${s.abstract}
|
||||
`
|
||||
).join('\n');
|
||||
}
|
||||
154
examples/research-team/types.ts
Normal file
154
examples/research-team/types.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Research Team Types
|
||||
*
|
||||
* Shared type definitions for the multi-agent research system.
|
||||
*/
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// CORE TYPES
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export type Depth = 'quick' | 'standard' | 'comprehensive';
|
||||
|
||||
export type AgentRole = 'coordinator' | 'researcher' | 'analyst' | 'writer';
|
||||
|
||||
export interface ResearchTask {
|
||||
id: string;
|
||||
query: string;
|
||||
depth: Depth;
|
||||
createdAt: Date;
|
||||
status: 'pending' | 'researching' | 'analyzing' | 'writing' | 'complete' | 'failed';
|
||||
}
|
||||
|
||||
export interface ResearchReport {
|
||||
taskId: string;
|
||||
query: string;
|
||||
depth: Depth;
|
||||
content: string;
|
||||
sourcesUsed: number;
|
||||
durationMs: number;
|
||||
completedAt: Date;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// DEPTH CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export interface DepthConfig {
|
||||
sourcesCount: number;
|
||||
analysisDepth: 'brief' | 'thorough' | 'extensive';
|
||||
reportSections: string[];
|
||||
maxIterations: number;
|
||||
estimatedMinutes: number;
|
||||
}
|
||||
|
||||
export const DEPTH_CONFIGS: Record<Depth, DepthConfig> = {
|
||||
quick: {
|
||||
sourcesCount: 3,
|
||||
analysisDepth: 'brief',
|
||||
reportSections: ['summary', 'key_findings', 'sources'],
|
||||
maxIterations: 1,
|
||||
estimatedMinutes: 5,
|
||||
},
|
||||
standard: {
|
||||
sourcesCount: 6,
|
||||
analysisDepth: 'thorough',
|
||||
reportSections: ['summary', 'background', 'key_findings', 'analysis', 'sources'],
|
||||
maxIterations: 2,
|
||||
estimatedMinutes: 15,
|
||||
},
|
||||
comprehensive: {
|
||||
sourcesCount: 10,
|
||||
analysisDepth: 'extensive',
|
||||
reportSections: ['executive_summary', 'background', 'methodology', 'findings', 'analysis', 'implications', 'future_directions', 'sources'],
|
||||
maxIterations: 3,
|
||||
estimatedMinutes: 30,
|
||||
},
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// SOURCE TYPES
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export interface AcademicSource {
|
||||
id: string;
|
||||
title: string;
|
||||
authors: string[];
|
||||
abstract: string;
|
||||
venue: string;
|
||||
year: number;
|
||||
citations: number;
|
||||
domain: string;
|
||||
quality: number; // 1-10
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface SourceEvaluation {
|
||||
sourceId: string;
|
||||
relevanceScore: number; // 1-10
|
||||
qualityScore: number; // 1-10
|
||||
notes: string;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// FEEDBACK TYPES
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export interface UserFeedback {
|
||||
taskId: string;
|
||||
rating: number; // 1-5 stars
|
||||
comment?: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface AgentReflection {
|
||||
taskId: string;
|
||||
agent: AgentRole;
|
||||
whatWorked: string;
|
||||
whatDidntWork: string;
|
||||
improvements: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// WORKFLOW TYPES
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export interface WorkflowPhase {
|
||||
name: string;
|
||||
agent: AgentRole;
|
||||
status: 'pending' | 'running' | 'complete' | 'failed';
|
||||
startedAt?: Date;
|
||||
completedAt?: Date;
|
||||
output?: string;
|
||||
}
|
||||
|
||||
export interface TeamState {
|
||||
agentIds: Record<AgentRole, string | null>;
|
||||
currentTask: ResearchTask | null;
|
||||
phases: WorkflowPhase[];
|
||||
sharedBlockIds: {
|
||||
sources: string | null;
|
||||
terminology: string | null;
|
||||
pitfalls: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// HELPER FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
export function generateTaskId(): string {
|
||||
return `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (minutes === 0) {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
Reference in New Issue
Block a user