feat(core,cli)!: Implement in-process agent backend for arenas

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Add InProcessBackend to run subagents in-process rather than via subprocess,

enabling faster initialization and better resource management for agent

collaboration arenas.

Key changes:

- Add InProcessBackend with sandboxed in-process agent execution

- Refactor agent runtime into headless vs interactive modes

- Add AsyncMessageQueue utility for agent message passing

- Update ArenaManager with backend selection (in-process vs subprocess)

- Refactor subagent types/exports; consolidate in subagents/types

- Remove deprecated agent-hooks.ts (functionality merged into runtime)

- Update task tool to support new agent lifecycle

Breaking: Subagent type exports restructured; import from subagents/types
This commit is contained in:
tanzhenxin 2026-02-21 21:08:20 +08:00
parent e968483a8a
commit d4cfb18f79
39 changed files with 2951 additions and 502 deletions

View file

@ -7,7 +7,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
type ArenaManager,
ArenaAgentStatus,
AgentStatus,
ArenaSessionStatus,
} from '@qwen-code/qwen-code-core';
import { arenaCommand } from './arenaCommand.js';
@ -242,7 +242,7 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.TERMINATED,
status: AgentStatus.FAILED,
model: { modelId: 'model-1' },
},
]),
@ -267,12 +267,12 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'model-1' },
},
{
agentId: 'agent-2',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'model-2' },
},
]),
@ -294,12 +294,12 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'gpt-4o', displayName: 'gpt-4o' },
},
{
agentId: 'agent-2',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'claude-sonnet', displayName: 'claude-sonnet' },
},
]),
@ -327,7 +327,7 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'gpt-4o', displayName: 'gpt-4o' },
},
]),
@ -350,7 +350,7 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'gpt-4o' },
},
]),
@ -373,7 +373,7 @@ describe('arenaCommand select subcommand', () => {
getAgentStates: vi.fn(() => [
{
agentId: 'agent-1',
status: ArenaAgentStatus.COMPLETED,
status: AgentStatus.COMPLETED,
model: { modelId: 'gpt-4o' },
},
]),

View file

@ -16,7 +16,8 @@ import { CommandKind } from './types.js';
import {
ArenaManager,
ArenaEventType,
ArenaAgentStatus,
AgentStatus,
isTerminalStatus,
ArenaSessionStatus,
AuthType,
createDebugLogger,
@ -246,41 +247,23 @@ function executeArenaCommand(
const buildAgentCardData = (
result: ArenaAgentCompleteEvent['result'],
): ArenaAgentCardData => {
let status: ArenaAgentCardData['status'];
switch (result.status) {
case ArenaAgentStatus.COMPLETED:
status = 'completed';
break;
case ArenaAgentStatus.CANCELLED:
status = 'cancelled';
break;
default:
status = 'terminated';
break;
}
return {
label: result.model.displayName || result.model.modelId,
status,
durationMs: result.stats.durationMs,
totalTokens: result.stats.totalTokens,
inputTokens: result.stats.inputTokens,
outputTokens: result.stats.outputTokens,
toolCalls: result.stats.toolCalls,
successfulToolCalls: result.stats.successfulToolCalls,
failedToolCalls: result.stats.failedToolCalls,
rounds: result.stats.rounds,
error: result.error,
diff: result.diff,
};
};
): ArenaAgentCardData => ({
label: result.model.displayName || result.model.modelId,
status: result.status,
durationMs: result.stats.durationMs,
totalTokens: result.stats.totalTokens,
inputTokens: result.stats.inputTokens,
outputTokens: result.stats.outputTokens,
toolCalls: result.stats.toolCalls,
successfulToolCalls: result.stats.successfulToolCalls,
failedToolCalls: result.stats.failedToolCalls,
rounds: result.stats.rounds,
error: result.error,
diff: result.diff,
});
const handleAgentComplete = (event: ArenaAgentCompleteEvent) => {
if (
event.result.status !== ArenaAgentStatus.COMPLETED &&
event.result.status !== ArenaAgentStatus.CANCELLED &&
event.result.status !== ArenaAgentStatus.TERMINATED
) {
if (!isTerminalStatus(event.result.status)) {
return;
}
@ -598,7 +581,7 @@ export const arenaCommand: SlashCommand = {
const agents = manager.getAgentStates();
const hasSuccessful = agents.some(
(a) => a.status === ArenaAgentStatus.COMPLETED,
(a) => a.status === AgentStatus.COMPLETED,
);
if (!hasSuccessful) {
@ -616,7 +599,7 @@ export const arenaCommand: SlashCommand = {
const matchingAgent = agents.find((a) => {
const label = a.model.displayName || a.model.modelId;
return (
a.status === ArenaAgentStatus.COMPLETED &&
a.status === AgentStatus.COMPLETED &&
(label.toLowerCase() === trimmedArgs.toLowerCase() ||
a.model.modelId.toLowerCase() === trimmedArgs.toLowerCase())
);

View file

@ -9,7 +9,7 @@ import { useCallback, useMemo } from 'react';
import { Box, Text } from 'ink';
import {
type ArenaManager,
ArenaAgentStatus,
AgentStatus,
type Config,
} from '@qwen-code/qwen-code-core';
import { theme } from '../semantic-colors.js';
@ -138,7 +138,7 @@ export function ArenaSelectDialog({
// Build diff summary from cached result if available
let diffAdditions = 0;
let diffDeletions = 0;
if (agent.status === ArenaAgentStatus.COMPLETED && result) {
if (agent.status === AgentStatus.COMPLETED && result) {
const agentResult = result.agents.find(
(a) => a.agentId === agent.agentId,
);
@ -182,7 +182,7 @@ export function ArenaSelectDialog({
value: agent.agentId,
title,
description,
disabled: agent.status !== ArenaAgentStatus.COMPLETED,
disabled: agent.status !== AgentStatus.COMPLETED,
};
}),
[agents, result],

View file

@ -10,7 +10,7 @@ import { Box, Text } from 'ink';
import {
type ArenaManager,
type ArenaAgentState,
ArenaAgentStatus,
isTerminalStatus,
ArenaSessionStatus,
} from '@qwen-code/qwen-code-core';
import { theme } from '../semantic-colors.js';
@ -42,11 +42,7 @@ function pad(
}
function getElapsedMs(agent: ArenaAgentState): number {
if (
agent.status === ArenaAgentStatus.COMPLETED ||
agent.status === ArenaAgentStatus.TERMINATED ||
agent.status === ArenaAgentStatus.CANCELLED
) {
if (isTerminalStatus(agent.status)) {
return agent.stats.durationMs;
}
return Date.now() - agent.startedAt;

View file

@ -11,6 +11,7 @@ import type {
ToolCallConfirmationDetails,
ToolConfirmationOutcome,
ToolResultDisplay,
AgentStatus,
} from '@qwen-code/qwen-code-core';
import type { PartListUnion } from '@google/genai';
import { type ReactNode } from 'react';
@ -266,7 +267,7 @@ export type HistoryItemMcpStatus = HistoryItemBase & {
*/
export interface ArenaAgentCardData {
label: string;
status: 'completed' | 'cancelled' | 'terminated';
status: AgentStatus;
durationMs: number;
totalTokens: number;
inputTokens: number;

View file

@ -5,7 +5,7 @@
*/
import { theme } from '../semantic-colors.js';
import { ArenaAgentStatus } from '@qwen-code/qwen-code-core';
import { AgentStatus } from '@qwen-code/qwen-code-core';
// --- Status Labels ---
@ -15,24 +15,17 @@ export interface StatusLabel {
color: string;
}
export function getArenaStatusLabel(
status: ArenaAgentStatus | string,
): StatusLabel {
export function getArenaStatusLabel(status: AgentStatus): StatusLabel {
switch (status) {
case ArenaAgentStatus.COMPLETED:
case 'completed':
case AgentStatus.COMPLETED:
return { icon: '✓', text: 'Done', color: theme.status.success };
case ArenaAgentStatus.CANCELLED:
case 'cancelled':
case AgentStatus.CANCELLED:
return { icon: '⊘', text: 'Cancelled', color: theme.status.warning };
case ArenaAgentStatus.TERMINATED:
case 'terminated':
return { icon: '✗', text: 'Terminated', color: theme.status.error };
case ArenaAgentStatus.RUNNING:
case 'running':
case AgentStatus.FAILED:
return { icon: '✗', text: 'Failed', color: theme.status.error };
case AgentStatus.RUNNING:
return { icon: '○', text: 'Running', color: theme.text.secondary };
case ArenaAgentStatus.INITIALIZING:
case 'initializing':
case AgentStatus.INITIALIZING:
return { icon: '○', text: 'Initializing', color: theme.text.secondary };
default:
return { icon: '○', text: status, color: theme.text.secondary };