feat(arena): Persist arena events to chat history and add progress updates

- Replace SESSION_WARNING with SESSION_UPDATE supporting info/warning types
- Emit setup progress messages from ArenaManager during agent initialization
- Record all arena UI events to session JSONL for chat history replay
- Clean up unused agent event types (stream, tool calls, stats)
- Update arena select/stop dialogs to record their output

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-18 14:33:37 +08:00
parent 6b55c8161f
commit 193bc438bd
8 changed files with 163 additions and 150 deletions

View file

@ -257,7 +257,7 @@ describe('arenaCommand select subcommand', () => {
messageType: 'error', messageType: 'error',
content: content:
'No successful agent results to select from. All agents failed or were cancelled.\n' + 'No successful agent results to select from. All agents failed or were cancelled.\n' +
'Use /arena select --discard to clean up worktrees, or /arena stop to end the session.', 'Use /arena stop to end the session.',
}); });
}); });

View file

@ -28,7 +28,7 @@ import {
type ArenaSessionCompleteEvent, type ArenaSessionCompleteEvent,
type ArenaSessionErrorEvent, type ArenaSessionErrorEvent,
type ArenaSessionStartEvent, type ArenaSessionStartEvent,
type ArenaSessionWarningEvent, type ArenaSessionUpdateEvent,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { import {
MessageType, MessageType,
@ -147,6 +147,26 @@ function buildArenaExecutionInput(
}; };
} }
/**
* Persists a single arena history item to the session JSONL file.
*
* Arena events fire asynchronously (after the slash command's recording
* window has closed), so each item must be recorded individually.
*/
function recordArenaItem(config: Config, item: HistoryItemWithoutId): void {
try {
const chatRecorder = config.getChatRecordingService();
if (!chatRecorder) return;
chatRecorder.recordSlashCommand({
phase: 'result',
rawCommand: '/arena',
outputHistoryItems: [{ ...item } as Record<string, unknown>],
});
} catch {
debugLogger.error('Failed to record arena history item');
}
}
function executeArenaCommand( function executeArenaCommand(
config: Config, config: Config,
ui: CommandContext['ui'], ui: CommandContext['ui'],
@ -164,6 +184,15 @@ function executeArenaCommand(
ui.addItem({ type, text }, Date.now()); ui.addItem({ type, text }, Date.now());
}; };
const addAndRecordArenaMessage = (
type: 'info' | 'warning' | 'error' | 'success',
text: string,
) => {
const item: HistoryItemWithoutId = { type, text };
ui.addItem(item, Date.now());
recordArenaItem(config, item);
};
const handleSessionStart = (event: ArenaSessionStartEvent) => { const handleSessionStart = (event: ArenaSessionStartEvent) => {
const modelList = event.models const modelList = event.models
.map( .map(
@ -171,6 +200,9 @@ function executeArenaCommand(
` ${index + 1}. ${model.displayName || model.modelId}`, ` ${index + 1}. ${model.displayName || model.modelId}`,
) )
.join('\n'); .join('\n');
// SESSION_START fires synchronously before the first await in
// ArenaManager.start(), so the slash command processor's finally
// block already captures this item — no extra recording needed.
addArenaMessage( addArenaMessage(
MessageType.INFO, MessageType.INFO,
`Arena started with ${event.models.length} agents on task: "${event.task}"\nModels:\n${modelList}`, `Arena started with ${event.models.length} agents on task: "${event.task}"\nModels:\n${modelList}`,
@ -183,22 +215,33 @@ function executeArenaCommand(
debugLogger.debug(`Arena agent started: ${label} (${event.agentId})`); debugLogger.debug(`Arena agent started: ${label} (${event.agentId})`);
}; };
const handleSessionWarning = (event: ArenaSessionWarningEvent) => { const handleSessionUpdate = (event: ArenaSessionUpdateEvent) => {
const attachHintPrefix = 'To view agent panes, run: '; const attachHintPrefix = 'To view agent panes, run: ';
if (event.message.startsWith(attachHintPrefix)) { if (event.message.startsWith(attachHintPrefix)) {
const command = event.message.slice(attachHintPrefix.length).trim(); const command = event.message.slice(attachHintPrefix.length).trim();
addArenaMessage( addAndRecordArenaMessage(
MessageType.INFO, MessageType.INFO,
`Arena panes are running in tmux. Attach with: \`${command}\``, `Arena panes are running in tmux. Attach with: \`${command}\``,
); );
return; return;
} }
addArenaMessage(MessageType.WARNING, `Arena warning: ${event.message}`);
if (event.type === 'info') {
addAndRecordArenaMessage(MessageType.INFO, event.message);
} else {
addAndRecordArenaMessage(
MessageType.WARNING,
`Arena warning: ${event.message}`,
);
}
}; };
const handleAgentError = (event: ArenaAgentErrorEvent) => { const handleAgentError = (event: ArenaAgentErrorEvent) => {
const label = agentLabels.get(event.agentId) || event.agentId; const label = agentLabels.get(event.agentId) || event.agentId;
addArenaMessage(MessageType.ERROR, `[${label}] failed: ${event.error}`); addAndRecordArenaMessage(
MessageType.ERROR,
`[${label}] failed: ${event.error}`,
);
}; };
const buildAgentCardData = ( const buildAgentCardData = (
@ -233,7 +276,6 @@ function executeArenaCommand(
}; };
const handleAgentComplete = (event: ArenaAgentCompleteEvent) => { const handleAgentComplete = (event: ArenaAgentCompleteEvent) => {
// Show message for completed (success), cancelled, and terminated (error) agents
if ( if (
event.result.status !== ArenaAgentStatus.COMPLETED && event.result.status !== ArenaAgentStatus.COMPLETED &&
event.result.status !== ArenaAgentStatus.CANCELLED && event.result.status !== ArenaAgentStatus.CANCELLED &&
@ -243,30 +285,28 @@ function executeArenaCommand(
} }
const agent = buildAgentCardData(event.result); const agent = buildAgentCardData(event.result);
ui.addItem( const item = {
{ type: 'arena_agent_complete',
type: 'arena_agent_complete', agent,
agent, } as HistoryItemWithoutId;
} as HistoryItemWithoutId, ui.addItem(item, Date.now());
Date.now(), recordArenaItem(config, item);
);
}; };
const handleSessionError = (event: ArenaSessionErrorEvent) => { const handleSessionError = (event: ArenaSessionErrorEvent) => {
addArenaMessage(MessageType.ERROR, `Arena failed: ${event.error}`); addAndRecordArenaMessage(MessageType.ERROR, `Arena failed: ${event.error}`);
}; };
const handleSessionComplete = (event: ArenaSessionCompleteEvent) => { const handleSessionComplete = (event: ArenaSessionCompleteEvent) => {
ui.addItem( const item = {
{ type: 'arena_session_complete',
type: 'arena_session_complete', sessionStatus: event.result.status,
sessionStatus: event.result.status, task: event.result.task,
task: event.result.task, totalDurationMs: event.result.totalDurationMs ?? 0,
totalDurationMs: event.result.totalDurationMs ?? 0, agents: event.result.agents.map(buildAgentCardData),
agents: event.result.agents.map(buildAgentCardData), } as HistoryItemWithoutId;
} as HistoryItemWithoutId, ui.addItem(item, Date.now());
Date.now(), recordArenaItem(config, item);
);
}; };
emitter.on(ArenaEventType.SESSION_START, handleSessionStart); emitter.on(ArenaEventType.SESSION_START, handleSessionStart);
@ -277,9 +317,9 @@ function executeArenaCommand(
detachListeners.push(() => detachListeners.push(() =>
emitter.off(ArenaEventType.AGENT_START, handleAgentStart), emitter.off(ArenaEventType.AGENT_START, handleAgentStart),
); );
emitter.on(ArenaEventType.SESSION_WARNING, handleSessionWarning); emitter.on(ArenaEventType.SESSION_UPDATE, handleSessionUpdate);
detachListeners.push(() => detachListeners.push(() =>
emitter.off(ArenaEventType.SESSION_WARNING, handleSessionWarning), emitter.off(ArenaEventType.SESSION_UPDATE, handleSessionUpdate),
); );
emitter.on(ArenaEventType.AGENT_ERROR, handleAgentError); emitter.on(ArenaEventType.AGENT_ERROR, handleAgentError);
detachListeners.push(() => detachListeners.push(() =>
@ -317,7 +357,7 @@ function executeArenaCommand(
}, },
(error) => { (error) => {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
addArenaMessage(MessageType.ERROR, `Arena failed: ${message}`); addAndRecordArenaMessage(MessageType.ERROR, `Arena failed: ${message}`);
debugLogger.error('Arena session failed:', error); debugLogger.error('Arena session failed:', error);
// Clear the stored manager so subsequent /arena start calls // Clear the stored manager so subsequent /arena start calls
@ -567,7 +607,7 @@ export const arenaCommand: SlashCommand = {
messageType: 'error', messageType: 'error',
content: content:
'No successful agent results to select from. All agents failed or were cancelled.\n' + 'No successful agent results to select from. All agents failed or were cancelled.\n' +
'Use /arena select --discard to clean up worktrees, or /arena stop to end the session.', 'Use /arena stop to end the session.',
}; };
} }

View file

@ -14,7 +14,7 @@ import {
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { MessageType } from '../types.js'; import { MessageType, type HistoryItemWithoutId } from '../types.js';
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
import { formatDuration } from '../utils/formatters.js'; import { formatDuration } from '../utils/formatters.js';
import { getArenaStatusLabel } from '../utils/displayUtils.js'; import { getArenaStatusLabel } from '../utils/displayUtils.js';
@ -36,18 +36,25 @@ export function ArenaSelectDialog({
}: ArenaSelectDialogProps): React.JSX.Element { }: ArenaSelectDialogProps): React.JSX.Element {
const pushMessage = useCallback( const pushMessage = useCallback(
(result: { messageType: 'info' | 'error'; content: string }) => { (result: { messageType: 'info' | 'error'; content: string }) => {
addItem( const item: HistoryItemWithoutId = {
{ type:
type: result.messageType === 'info' ? MessageType.INFO : MessageType.ERROR,
result.messageType === 'info' text: result.content,
? MessageType.INFO };
: MessageType.ERROR, addItem(item, Date.now());
text: result.content,
}, try {
Date.now(), const chatRecorder = config.getChatRecordingService();
); chatRecorder?.recordSlashCommand({
phase: 'result',
rawCommand: '/arena select',
outputHistoryItems: [{ ...item } as Record<string, unknown>],
});
} catch {
// Best-effort recording
}
}, },
[addItem], [addItem, config],
); );
const onSelect = useCallback( const onSelect = useCallback(

View file

@ -14,7 +14,7 @@ import {
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { useKeypress } from '../hooks/useKeypress.js'; import { useKeypress } from '../hooks/useKeypress.js';
import { MessageType } from '../types.js'; import { MessageType, type HistoryItemWithoutId } from '../types.js';
import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js'; import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
import type { DescriptiveRadioSelectItem } from './shared/DescriptiveRadioButtonSelect.js'; import type { DescriptiveRadioSelectItem } from './shared/DescriptiveRadioButtonSelect.js';
@ -38,18 +38,25 @@ export function ArenaStopDialog({
const pushMessage = useCallback( const pushMessage = useCallback(
(result: { messageType: 'info' | 'error'; content: string }) => { (result: { messageType: 'info' | 'error'; content: string }) => {
addItem( const item: HistoryItemWithoutId = {
{ type:
type: result.messageType === 'info' ? MessageType.INFO : MessageType.ERROR,
result.messageType === 'info' text: result.content,
? MessageType.INFO };
: MessageType.ERROR, addItem(item, Date.now());
text: result.content,
}, try {
Date.now(), const chatRecorder = config.getChatRecordingService();
); chatRecorder?.recordSlashCommand({
phase: 'result',
rawCommand: '/arena stop',
outputHistoryItems: [{ ...item } as Record<string, unknown>],
});
} catch {
// Best-effort recording
}
}, },
[addItem], [addItem, config],
); );
const onStop = useCallback( const onStop = useCallback(

View file

@ -35,7 +35,7 @@ export const ArenaAgentCard: React.FC<ArenaAgentCardProps> = ({
{/* Line 1: Status icon + text + label + duration */} {/* Line 1: Status icon + text + label + duration */}
<Box> <Box>
<Text color={color}> <Text color={color}>
{icon} {text}: {agent.label} · {duration} {icon} {agent.label} · {text} · {duration}
</Text> </Text>
</Box> </Box>

View file

@ -272,11 +272,19 @@ describe('ArenaManager', () => {
}); });
describe('backend initialization', () => { describe('backend initialization', () => {
it('should emit SESSION_WARNING when backend detection returns warning', async () => { it('should emit SESSION_UPDATE with type warning when backend detection returns warning', async () => {
const manager = new ArenaManager(mockConfig as never); const manager = new ArenaManager(mockConfig as never);
const warnings: Array<{ message: string; sessionId: string }> = []; const updates: Array<{
manager.getEventEmitter().on(ArenaEventType.SESSION_WARNING, (event) => { type: string;
warnings.push({ message: event.message, sessionId: event.sessionId }); message: string;
sessionId: string;
}> = [];
manager.getEventEmitter().on(ArenaEventType.SESSION_UPDATE, (event) => {
updates.push({
type: event.type,
message: event.message,
sessionId: event.sessionId,
});
}); });
hoistedMockDetectBackend.mockResolvedValueOnce({ hoistedMockDetectBackend.mockResolvedValueOnce({
@ -287,9 +295,10 @@ describe('ArenaManager', () => {
await manager.start(createValidStartOptions()); await manager.start(createValidStartOptions());
expect(hoistedMockDetectBackend).toHaveBeenCalledWith(undefined); expect(hoistedMockDetectBackend).toHaveBeenCalledWith(undefined);
expect(warnings).toHaveLength(1); const warningUpdate = updates.find((u) => u.type === 'warning');
expect(warnings[0]?.message).toContain('fallback to tmux backend'); expect(warningUpdate).toBeDefined();
expect(warnings[0]?.sessionId).toMatch(/^arena-/); expect(warningUpdate?.message).toContain('fallback to tmux backend');
expect(warningUpdate?.sessionId).toMatch(/^arena-/);
}); });
it('should emit SESSION_ERROR and mark FAILED when backend init fails', async () => { it('should emit SESSION_ERROR and mark FAILED when backend init fails', async () => {

View file

@ -302,6 +302,7 @@ export class ArenaManager {
} }
// Set up worktrees for all agents // Set up worktrees for all agents
this.emitProgress(`Setting up environment for agents…`);
await this.setupWorktrees(); await this.setupWorktrees();
// If cancelled during worktree setup, bail out early // If cancelled during worktree setup, bail out early
@ -311,6 +312,7 @@ export class ArenaManager {
} }
// Start all agents in parallel via PTY // Start all agents in parallel via PTY
this.emitProgress('Environment ready. Launching agents…');
this.sessionStatus = ArenaSessionStatus.RUNNING; this.sessionStatus = ArenaSessionStatus.RUNNING;
await this.runAgents(); await this.runAgents();
@ -474,6 +476,22 @@ export class ArenaManager {
return this.worktreeService.getWorktreeDiff(agent.worktree.path); return this.worktreeService.getWorktreeDiff(agent.worktree.path);
} }
// ─── Private: Progress ─────────────────────────────────────────
/**
* Emit a progress message via SESSION_UPDATE so the UI can display
* setup status.
*/
private emitProgress(message: string): void {
if (!this.sessionId) return;
this.eventEmitter.emit(ArenaEventType.SESSION_UPDATE, {
sessionId: this.sessionId,
type: 'info',
message,
timestamp: Date.now(),
});
}
// ─── Private: Validation ─────────────────────────────────────── // ─── Private: Validation ───────────────────────────────────────
private validateStartOptions(options: ArenaStartOptions): void { private validateStartOptions(options: ArenaStartOptions): void {
@ -524,8 +542,9 @@ export class ArenaManager {
this.backend = backend; this.backend = backend;
if (warning && this.sessionId) { if (warning && this.sessionId) {
this.eventEmitter.emit(ArenaEventType.SESSION_WARNING, { this.eventEmitter.emit(ArenaEventType.SESSION_UPDATE, {
sessionId: this.sessionId, sessionId: this.sessionId,
type: 'warning',
message: warning, message: warning,
timestamp: Date.now(), timestamp: Date.now(),
}); });
@ -534,8 +553,9 @@ export class ArenaManager {
// Surface attach hint for external tmux sessions // Surface attach hint for external tmux sessions
const attachHint = backend.getAttachHint(); const attachHint = backend.getAttachHint();
if (attachHint && this.sessionId) { if (attachHint && this.sessionId) {
this.eventEmitter.emit(ArenaEventType.SESSION_WARNING, { this.eventEmitter.emit(ArenaEventType.SESSION_UPDATE, {
sessionId: this.sessionId, sessionId: this.sessionId,
type: 'info',
message: `To view agent panes, run: ${attachHint}`, message: `To view agent panes, run: ${attachHint}`,
timestamp: Date.now(), timestamp: Date.now(),
}); });
@ -1045,14 +1065,6 @@ export class ArenaManager {
this.updateAgentStatus(agent.agentId, ArenaAgentStatus.RUNNING); this.updateAgentStatus(agent.agentId, ArenaAgentStatus.RUNNING);
} }
// Emit stats update event
this.eventEmitter.emit(ArenaEventType.AGENT_STATS_UPDATE, {
sessionId: this.requireConfig().sessionId,
agentId: agent.agentId,
stats: statusFile.stats,
timestamp: Date.now(),
});
this.callbacks.onAgentStatsUpdate?.(agent.agentId, statusFile.stats); this.callbacks.onAgentStatsUpdate?.(agent.agentId, statusFile.stats);
} catch (error: unknown) { } catch (error: unknown) {
// File may not exist yet (agent hasn't written first status) // File may not exist yet (agent hasn't written first status)

View file

@ -8,7 +8,6 @@ import { EventEmitter } from 'events';
import type { import type {
ArenaAgentStatus, ArenaAgentStatus,
ArenaModelConfig, ArenaModelConfig,
ArenaAgentStats,
ArenaAgentResult, ArenaAgentResult,
ArenaSessionResult, ArenaSessionResult,
} from './types.js'; } from './types.js';
@ -19,6 +18,8 @@ import type {
export enum ArenaEventType { export enum ArenaEventType {
/** Arena session started */ /** Arena session started */
SESSION_START = 'session_start', SESSION_START = 'session_start',
/** Informational or warning update during session lifecycle */
SESSION_UPDATE = 'session_update',
/** Arena session completed */ /** Arena session completed */
SESSION_COMPLETE = 'session_complete', SESSION_COMPLETE = 'session_complete',
/** Arena session failed */ /** Arena session failed */
@ -27,35 +28,21 @@ export enum ArenaEventType {
AGENT_START = 'agent_start', AGENT_START = 'agent_start',
/** Agent status changed */ /** Agent status changed */
AGENT_STATUS_CHANGE = 'agent_status_change', AGENT_STATUS_CHANGE = 'agent_status_change',
/** Agent streamed text */
AGENT_STREAM_TEXT = 'agent_stream_text',
/** Agent called a tool */
AGENT_TOOL_CALL = 'agent_tool_call',
/** Agent tool call completed */
AGENT_TOOL_RESULT = 'agent_tool_result',
/** Agent stats updated */
AGENT_STATS_UPDATE = 'agent_stats_update',
/** Agent completed */ /** Agent completed */
AGENT_COMPLETE = 'agent_complete', AGENT_COMPLETE = 'agent_complete',
/** Agent error */ /** Agent error */
AGENT_ERROR = 'agent_error', AGENT_ERROR = 'agent_error',
/** Non-fatal warning (e.g., backend fallback) */
SESSION_WARNING = 'session_warning',
} }
export type ArenaEvent = export type ArenaEvent =
| 'session_start' | 'session_start'
| 'session_update'
| 'session_complete' | 'session_complete'
| 'session_error' | 'session_error'
| 'agent_start' | 'agent_start'
| 'agent_status_change' | 'agent_status_change'
| 'agent_stream_text'
| 'agent_tool_call'
| 'agent_tool_result'
| 'agent_stats_update'
| 'agent_complete' | 'agent_complete'
| 'agent_error' | 'agent_error';
| 'session_warning';
/** /**
* Event payload for session start. * Event payload for session start.
@ -97,61 +84,12 @@ export interface ArenaAgentStartEvent {
} }
/** /**
* Event payload for agent status change. * Event payload for agent error.
*/ */
export interface ArenaAgentStatusChangeEvent { export interface ArenaAgentErrorEvent {
sessionId: string; sessionId: string;
agentId: string; agentId: string;
previousStatus: ArenaAgentStatus; error: string;
newStatus: ArenaAgentStatus;
timestamp: number;
}
/**
* Event payload for agent stream text.
*/
export interface ArenaAgentStreamTextEvent {
sessionId: string;
agentId: string;
text: string;
isThought?: boolean;
timestamp: number;
}
/**
* Event payload for agent tool call.
*/
export interface ArenaAgentToolCallEvent {
sessionId: string;
agentId: string;
callId: string;
toolName: string;
args: Record<string, unknown>;
description?: string;
timestamp: number;
}
/**
* Event payload for agent tool result.
*/
export interface ArenaAgentToolResultEvent {
sessionId: string;
agentId: string;
callId: string;
toolName: string;
success: boolean;
error?: string;
durationMs: number;
timestamp: number;
}
/**
* Event payload for agent stats update.
*/
export interface ArenaAgentStatsUpdateEvent {
sessionId: string;
agentId: string;
stats: Partial<ArenaAgentStats>;
timestamp: number; timestamp: number;
} }
@ -166,20 +104,24 @@ export interface ArenaAgentCompleteEvent {
} }
/** /**
* Event payload for agent error. * Event payload for agent status change.
*/ */
export interface ArenaAgentErrorEvent { export interface ArenaAgentStatusChangeEvent {
sessionId: string; sessionId: string;
agentId: string; agentId: string;
error: string; previousStatus: ArenaAgentStatus;
newStatus: ArenaAgentStatus;
timestamp: number; timestamp: number;
} }
/** /**
* Event payload for session warning (non-fatal). * Event payload for session update (informational or warning).
*/ */
export interface ArenaSessionWarningEvent { export type ArenaSessionUpdateType = 'info' | 'warning';
export interface ArenaSessionUpdateEvent {
sessionId: string; sessionId: string;
type: ArenaSessionUpdateType;
message: string; message: string;
timestamp: number; timestamp: number;
} }
@ -189,17 +131,13 @@ export interface ArenaSessionWarningEvent {
*/ */
export interface ArenaEventMap { export interface ArenaEventMap {
[ArenaEventType.SESSION_START]: ArenaSessionStartEvent; [ArenaEventType.SESSION_START]: ArenaSessionStartEvent;
[ArenaEventType.SESSION_UPDATE]: ArenaSessionUpdateEvent;
[ArenaEventType.SESSION_COMPLETE]: ArenaSessionCompleteEvent; [ArenaEventType.SESSION_COMPLETE]: ArenaSessionCompleteEvent;
[ArenaEventType.SESSION_ERROR]: ArenaSessionErrorEvent; [ArenaEventType.SESSION_ERROR]: ArenaSessionErrorEvent;
[ArenaEventType.AGENT_START]: ArenaAgentStartEvent; [ArenaEventType.AGENT_START]: ArenaAgentStartEvent;
[ArenaEventType.AGENT_STATUS_CHANGE]: ArenaAgentStatusChangeEvent; [ArenaEventType.AGENT_STATUS_CHANGE]: ArenaAgentStatusChangeEvent;
[ArenaEventType.AGENT_STREAM_TEXT]: ArenaAgentStreamTextEvent;
[ArenaEventType.AGENT_TOOL_CALL]: ArenaAgentToolCallEvent;
[ArenaEventType.AGENT_TOOL_RESULT]: ArenaAgentToolResultEvent;
[ArenaEventType.AGENT_STATS_UPDATE]: ArenaAgentStatsUpdateEvent;
[ArenaEventType.AGENT_COMPLETE]: ArenaAgentCompleteEvent; [ArenaEventType.AGENT_COMPLETE]: ArenaAgentCompleteEvent;
[ArenaEventType.AGENT_ERROR]: ArenaAgentErrorEvent; [ArenaEventType.AGENT_ERROR]: ArenaAgentErrorEvent;
[ArenaEventType.SESSION_WARNING]: ArenaSessionWarningEvent;
} }
/** /**