breakout-command.ts
1 import type { Chatroom, ChatroomMessage } from '@/types' 2 import type { StructuredSessionLaunchContext } from '@/components/protocols/structured-session-launcher' 3 4 export const BREAKOUT_COMMAND = '/breakout' 5 6 export type BreakoutCommandParseResult = 7 | { kind: 'none'; query: ''; topic: '' } 8 | { kind: 'candidate'; query: string; topic: '' } 9 | { kind: 'command'; query: 'breakout'; topic: string } 10 11 const MAX_TITLE_LENGTH = 72 12 const MAX_MESSAGE_SNIPPET_LENGTH = 220 13 const MAX_KICKOFF_LENGTH = 1100 14 const MAX_KICKOFF_MESSAGES = 6 15 16 function compactWhitespace(value: string): string { 17 return value.replace(/\s+/g, ' ').trim() 18 } 19 20 function truncate(value: string, maxLength: number): string { 21 if (value.length <= maxLength) return value 22 return `${value.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...` 23 } 24 25 function summarizeMessage(message: ChatroomMessage): string { 26 const text = compactWhitespace(message.text || '') 27 if (text) return truncate(text, MAX_MESSAGE_SNIPPET_LENGTH) 28 29 const attachmentCount = (message.attachedFiles?.length || 0) + (message.imagePath ? 1 : 0) 30 if (attachmentCount > 0) { 31 return attachmentCount === 1 ? '[shared attachment]' : `[shared ${attachmentCount} attachments]` 32 } 33 34 return '' 35 } 36 37 export function parseBreakoutCommand(value: string): BreakoutCommandParseResult { 38 const normalized = value.replace(/\r/g, '') 39 if (normalized.includes('\n')) return { kind: 'none', query: '', topic: '' } 40 41 const trimmed = normalized.trim() 42 if (!trimmed.startsWith('/')) return { kind: 'none', query: '', topic: '' } 43 44 const withoutSlash = trimmed.slice(1) 45 const firstSpace = withoutSlash.indexOf(' ') 46 const commandToken = (firstSpace === -1 ? withoutSlash : withoutSlash.slice(0, firstSpace)).toLowerCase() 47 const topic = firstSpace === -1 ? '' : compactWhitespace(withoutSlash.slice(firstSpace + 1)) 48 49 if (!commandToken) return { kind: 'candidate', query: '', topic: '' } 50 if (BREAKOUT_COMMAND.slice(1).startsWith(commandToken)) { 51 if (commandToken === BREAKOUT_COMMAND.slice(1)) { 52 return { kind: 'command', query: 'breakout', topic } 53 } 54 return { kind: 'candidate', query: commandToken, topic: '' } 55 } 56 57 return { kind: 'none', query: '', topic: '' } 58 } 59 60 export function completeBreakoutCommand(value: string): string { 61 const parsed = parseBreakoutCommand(value) 62 if (parsed.kind === 'command') { 63 return parsed.topic ? `${BREAKOUT_COMMAND} ${parsed.topic}` : `${BREAKOUT_COMMAND} ` 64 } 65 return `${BREAKOUT_COMMAND} ` 66 } 67 68 export function buildBreakoutTitle(chatroomName: string | null | undefined, topic: string): string { 69 const compactTopic = compactWhitespace(topic) 70 if (compactTopic) return truncate(`Breakout: ${compactTopic}`, MAX_TITLE_LENGTH) 71 72 const fallback = compactWhitespace(chatroomName || '') || 'Current chatroom' 73 return truncate(`Breakout: ${fallback}`, MAX_TITLE_LENGTH) 74 } 75 76 export function buildBreakoutKickoffContext(messages: ChatroomMessage[]): string { 77 const candidates = messages 78 .filter((message) => message.senderId !== 'system' && message.historyExcluded !== true) 79 .map((message) => { 80 const summary = summarizeMessage(message) 81 if (!summary) return null 82 return `${message.senderName}: ${summary}` 83 }) 84 .filter(Boolean) as string[] 85 86 if (candidates.length === 0) return '' 87 88 const chosen: string[] = [] 89 let totalLength = 0 90 91 for (let index = candidates.length - 1; index >= 0; index -= 1) { 92 const entry = candidates[index] 93 const nextLength = totalLength + entry.length + (chosen.length > 0 ? 2 : 0) 94 if (chosen.length >= MAX_KICKOFF_MESSAGES || (nextLength > MAX_KICKOFF_LENGTH && chosen.length > 0)) { 95 break 96 } 97 chosen.unshift(entry) 98 totalLength = nextLength 99 } 100 101 return `Recent room context:\n${chosen.join('\n\n')}` 102 } 103 104 export function buildBreakoutLaunchContext( 105 chatroom: Pick<Chatroom, 'id' | 'name' | 'agentIds' | 'messages'>, 106 topic: string, 107 ): StructuredSessionLaunchContext { 108 return { 109 parentChatroomId: chatroom.id, 110 parentChatroomLabel: chatroom.name, 111 participantAgentIds: [...chatroom.agentIds], 112 facilitatorAgentId: chatroom.agentIds[0] || null, 113 title: buildBreakoutTitle(chatroom.name, topic), 114 goal: compactWhitespace(topic), 115 kickoffMessage: buildBreakoutKickoffContext(chatroom.messages), 116 autoStart: true, 117 createTranscript: true, 118 } 119 }