mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
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:
parent
6b55c8161f
commit
193bc438bd
8 changed files with 163 additions and 150 deletions
|
|
@ -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.',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue