import { PersonaPacket, PersonaMessage, PersonaReasoning, PersonaTransaction, PersonaProtocol, TransformMessages, AgentState, AgentEndPayload, ToolExecutionStartPayload, ToolExecutionUpdatePayload, ToolExecutionEndPayload, SteerAcceptedPayload, SteerProcessedPayload, MessageNotice, } from '../types'; import { parseMessages } from '../messages'; /** * Messaggio di servizio (turno interrotto, errore): non è testo prodotto * dall'agente, ed è marcato perché il client possa renderlo diversamente. */ function createNotice(kind: MessageNotice, text: string): PersonaMessage { return { role: 'assistant', type: 'text', text, createdAt: new Date(), finishReason: 'stop', status: { type: 'incomplete' }, metadata: { custom: { notice: kind } }, }; } /** * Creates packet listener for a protocol. * * Uses setAgentState instead of setIsRunning for proper lifecycle management. * The send button is only re-enabled after agent_end. */ export function createPacketListener( protocol: PersonaProtocol, setMessages: React.Dispatch>, setAgentState: React.Dispatch>, protocols: PersonaProtocol[], transformMessages?: TransformMessages, onSteerAccepted?: (payload: SteerAcceptedPayload) => void, onSteerProcessed?: (payload: SteerProcessedPayload) => void, ) { /** * Chiave dell'ultimo agent_end gestito: lo stesso evento può arrivare due * volte (riconnessione: il server rigioca gli eventi già emessi), e l'avviso * di fine turno non va aggiunto in chat una seconda volta. */ let lastAgentEndKey: string | null = null; return (packet: PersonaPacket) => { switch (packet.type) { // ─── Lifecycle: Agent ─────────────────────────────────────── case 'agent_start': lastAgentEndKey = null; setAgentState('running'); break; case 'agent_end': { const endPayload = packet.payload as unknown as AgentEndPayload; setAgentState('idle'); const endKey = `${endPayload.sessionId}|${endPayload.stopReason}|${endPayload.totalTurns}`; if (endKey === lastAgentEndKey) { break; } lastAgentEndKey = endKey; // Avviso in chat per gli stop reason che l'utente deve vedere. Il testo // è solo un fallback: `metadata.custom.notice` dice al client che non è // una risposta dell'agente ma un avviso di sistema, da rendere come // tale (persona-chat ne fa una riga discreta invece di una bolla). // In voce lo stop è un barge-in: interrompe il turno e basta, come // parlare sopra l'agente. Segnarlo in trascrizione sarebbe rumore. if (endPayload.stopReason === 'cancelled' && !isVoiceProtocol(protocol)) { setMessages((current) => [...current, createNotice('cancelled', 'Task interrupted')]); } else if (endPayload.stopReason === 'error') { setMessages((current) => [ ...current, createNotice('error', endPayload.errorMessage || 'Unknown error'), ]); } break; } // ─── Lifecycle: Turn ──────────────────────────────────────── case 'turn_start': // Opzionale: indicatore di nuovo turno nell'UI break; case 'turn_end': // Opzionale: riepilogo turno break; // ─── Lifecycle: Tool ──────────────────────────────────────── case 'tool_execution_start': handleToolStart( packet.payload as unknown as ToolExecutionStartPayload, protocol, setMessages, ); break; case 'tool_execution_update': handleToolUpdate( packet.payload as unknown as ToolExecutionUpdatePayload, setMessages, ); break; case 'tool_execution_end': handleToolEnd( packet.payload as unknown as ToolExecutionEndPayload, protocol, setMessages, ); break; // ─── Steer ────────────────────────────────────────────────── case 'steer_accepted': onSteerAccepted?.(packet.payload as unknown as SteerAcceptedPayload); break; case 'steer_processed': onSteerProcessed?.(packet.payload as unknown as SteerProcessedPayload); break; // ─── Messages ─────────────────────────────────────────────── case 'message': handleMessagePacket( packet.payload as unknown as PersonaMessage, protocol, setMessages, transformMessages, ); break; case 'reasoning': handleReasoningPacket( packet.payload as unknown as PersonaReasoning, protocol, setMessages, transformMessages, ); break; case 'transaction': handleTransactionPacket( packet.payload as unknown as PersonaTransaction, protocol, protocols, ); break; default: // Ignora packet types sconosciuti (forward compatibility) break; } }; } /** * I protocolli vocali non hanno una trascrizione da annotare: i messaggi di * sistema che in chat testuale spiegano cosa è successo, lì sono rumore. */ function isVoiceProtocol(protocol: PersonaProtocol): boolean { return protocol.getName() === 'webrtc'; } function handleMessagePacket( personaMessage: PersonaMessage, protocol: PersonaProtocol, setMessages: React.Dispatch>, transformMessages?: TransformMessages, ) { // Ensure message has proper timestamp if (!personaMessage.createdAt) { personaMessage.createdAt = new Date(); } // Convert thought to reasoning type if (personaMessage.thought) { personaMessage.type = 'reasoning'; } // Add protocol information personaMessage.protocol = protocol.getName(); setMessages((currentConversation) => { const newMessages = parseMessages([...currentConversation, personaMessage]); return transformMessages ? transformMessages(newMessages) : newMessages; }); } function handleReasoningPacket( reasoning: PersonaReasoning, protocol: PersonaProtocol, setMessages: React.Dispatch>, transformMessages?: TransformMessages, ) { const personaMessage: PersonaMessage = { type: 'reasoning', text: reasoning.thought, role: 'assistant', finishReason: 'stop', protocol: protocol.getName(), createdAt: new Date(), }; setMessages((currentConversation) => { const newMessages = parseMessages([...currentConversation, personaMessage]); return transformMessages ? transformMessages(newMessages) : newMessages; }); } function handleTransactionPacket( transaction: PersonaTransaction, currentProtocol: PersonaProtocol, protocols: PersonaProtocol[], ) { protocols .filter((p) => p !== currentProtocol) .forEach((p) => p.onTransaction(transaction)); } // ─── Tool Execution Handlers ────────────────────────────────────────── // Log di debug attivi solo quando l'host imposta window.__PERSONA_DEBUG__. const isPersonaDebug = (): boolean => typeof window !== 'undefined' && (window as unknown as { __PERSONA_DEBUG__?: boolean }).__PERSONA_DEBUG__ === true; function handleToolStart( payload: ToolExecutionStartPayload, protocol: PersonaProtocol, _setMessages: React.Dispatch>, ) { // Opzionale: crea un messaggio "tool in esecuzione" visibile nell'UI // Per ora, il tool call viene gestito dal flusso functionCalls esistente. // Qui possiamo emettere un log o un indicatore. if (isPersonaDebug()) { console.debug( `[${protocol.getName()}] Tool start: ${payload.toolName}`, payload.args, ); } } function handleToolUpdate( payload: ToolExecutionUpdatePayload, _setMessages: React.Dispatch>, ) { // Tool streaming update: aggiorna il risultato parziale // Per ora logghiamo; in futuro si può aggiornare un messaggio tool inline if (isPersonaDebug()) { console.debug( `Tool update: ${payload.toolCallId}`, payload.partialResult, ); } } function handleToolEnd( payload: ToolExecutionEndPayload, protocol: PersonaProtocol, _setMessages: React.Dispatch>, ) { // Tool execution completata. Il result arriva anche come functionResponse // nel flusso messaggi. Qui logghiamo per debug. if (isPersonaDebug()) { console.debug( `[${protocol.getName()}] Tool end: ${payload.toolCallId}`, payload.durationMs ? `(${payload.durationMs}ms)` : '', payload.error ? `ERROR: ${payload.error}` : '', ); } }