Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a814911
feat(core): add native multi-agent coordination
yiliang114 Aug 9, 2026
0dbb2f3
Merge remote-tracking branch 'origin/main' into cx/8718-native-coordi…
yiliang114 Aug 9, 2026
a6f51d5
feat(cli): add agent view pty workers
Jul 22, 2026
b38a42d
fix(cli): harden agent view pty workers
Jul 28, 2026
07cd280
fix(cli): harden agent view pty host teardown
Aug 3, 2026
842414b
fix(cli): fail fast on pty host auth rejection
Aug 3, 2026
5035f0d
test(cli): cover pty host remote exit polling
Aug 3, 2026
c5a1551
fix(cli): address agent view pty worker review nits
Aug 5, 2026
4808177
fix(cli): harden pty host socket fallback
Aug 5, 2026
885d38f
fix(cli): harden agent view pty workers
Aug 6, 2026
70f35ed
fix(cli): harden agent view pty workers
Aug 6, 2026
4f7c778
test(cli): cover pty host spawn contract
Aug 6, 2026
92553ec
fix(cli): guard agent view pty host socket takeover and races
qwen-code-ci-bot Aug 8, 2026
f0d4271
fix(cli): fit agent view pty logs under wire cap, strip host token af…
qwen-code-ci-bot Aug 8, 2026
a7ead9d
feat(cli): manage agent view session lifecycle
Jul 22, 2026
4ed6fb8
fix(cli): preserve agent view lifecycle state
Jul 28, 2026
54f26e5
fix(cli): harden agent view lifecycle persistence
Aug 5, 2026
95c48ca
fix(cli): harden agent view lifecycle
Aug 6, 2026
a60e6e7
fix(cli): harden agent view lifecycle recovery
Aug 7, 2026
4d29756
feat(cli): expose agent view commands
Jul 22, 2026
ac3f04b
fix(cli): wire agent view command safeguards
Jul 28, 2026
ede1ef1
fix(cli): resolve agent view command integration
yiliang114 Aug 9, 2026
995d67b
feat(cli): add agent view roster ui
Jul 22, 2026
038ee7e
feat(cli): add durable multi-agent coordination
yiliang114 Aug 9, 2026
9949ffd
fix(cli): harden multi-agent coordination
yiliang114 Aug 9, 2026
9a06509
fix(cli): complete native coordination flows
yiliang114 Aug 9, 2026
b9b4125
fix(cli): wait for agent view host cold starts
yiliang114 Aug 9, 2026
ddaba5c
fix(cli): allow coordination startup time
yiliang114 Aug 9, 2026
f17df3c
fix(cli): persist missing coordination results
yiliang114 Aug 9, 2026
b511e72
fix(cli): enforce exact Agent View answers
yiliang114 Aug 9, 2026
0b34b4e
refactor(cli): reuse existing agent team coordination
yiliang114 Aug 9, 2026
46066f1
Merge remote-tracking branch 'origin/main' into cx/8718-native-coordi…
yiliang114 Aug 9, 2026
9b166f7
docs(cli): clarify homogeneous coordination
yiliang114 Aug 9, 2026
8edf97f
Merge remote-tracking branch 'origin/main' into cx/8718-native-coordi…
yiliang114 Aug 10, 2026
fd84ca9
feat(core): complete native team coordination
yiliang114 Aug 10, 2026
3c79fcb
Merge remote-tracking branch 'origin/main' into cx/8718-native-coordi…
yiliang114 Aug 10, 2026
697f3c3
fix(agents): enforce teammate coordination boundaries
yiliang114 Aug 10, 2026
6c5704d
fix(agents): close teammate lifecycle gaps
yiliang114 Aug 10, 2026
cae7e4e
fix(agents): align empty teammate names with routing
yiliang114 Aug 10, 2026
f71e0f3
fix(core): close coordinator review gaps
yiliang114 Aug 10, 2026
b2614ae
fix(core): keep read-only teammates off writer tasks
yiliang114 Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/users/features/_meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default {
'tool-use-summaries': 'Tool-Use Summaries',
'markdown-rendering': 'Markdown Rendering',
'sub-agents': 'SubAgents',
'multi-agent-coordination': 'Multi-Agent Coordination',
arena: 'Agent Arena',
skills: 'Skills',
memory: 'Memory',
Expand Down
7 changes: 4 additions & 3 deletions docs/users/features/arena.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,14 @@ Agent Arena is experimental. Current limitations:

## Comparison with other multi-agent modes

Agent Arena is one of several planned multi-agent modes in Qwen Code. **Agent Team** and **Agent Swarm** are not yet implemented — the table below describes their intended design for reference.
Agent Arena and the experimental Agent Team runtime serve different multi-agent workflows. Agent Swarm remains a planned mode.

| | **Agent Arena** | **Agent Team** (planned) | **Agent Swarm** (planned) |
| | **Agent Arena** | **Agent Team** | **Agent Swarm** (planned) |
| :---------------- | :----------------------------------------------------- | :------------------------------------------------- | :------------------------------------------------------- |
| **Goal** | Competitive: Find the best solution to the _same_ task | Collaborative: Tackle _different_ aspects together | Batch parallel: Dynamically spawn workers for bulk tasks |
| **Agents** | Pre-configured models compete independently | Teammates collaborate with assigned roles | Workers spawned on-the-fly, destroyed on completion |
| **Communication** | No inter-agent communication | Direct peer-to-peer messaging | One-way: results aggregated by parent |
| **Isolation** | Full: separate Git worktrees | Independent sessions with shared task list | Lightweight ephemeral context per worker |
| **Isolation** | Full: separate Git worktrees | In-process teammates with a shared task list | Lightweight ephemeral context per worker |
| **Output** | One selected solution applied to workspace | Synthesized results from multiple perspectives | Aggregated results from parallel processing |
| **Best for** | Benchmarking, choosing between model approaches | Research, complex collaboration, cross-layer work | Batch operations, data processing, map-reduce tasks |

Expand All @@ -216,4 +216,5 @@ Agent Arena is one of several planned multi-agent modes in Qwen Code. **Agent Te
Explore related approaches for parallel and delegated work:

- **Lightweight delegation**: [Subagents](./sub-agents.md) handle focused subtasks within your session — better when you don't need model comparison
- **Collaborative execution**: [Multi-Agent Coordination](./multi-agent-coordination.md) uses Agent Team for shared tasks and teammate messaging
- **Manual parallel sessions**: Run multiple Qwen Code sessions yourself in separate terminals with [Git worktrees](https://git-scm.com/docs/git-worktree) for full manual control
13 changes: 7 additions & 6 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,13 @@ Commands for managing AI tools and models.

These commands invoke bundled skills that provide specialized workflows.

| Command | Description | Usage Examples |
| ------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------- |
| `/review` | Multi-agent code review (12 parallel agents at high effort) | `/review`, `/review 123`, `/review 123 --comment`, `/review --effort low` |
| `/loop` | Run a prompt on a recurring schedule | `/loop 5m check the build` |
| `/simplify` | Review recent changes and apply safe cleanup edits directly | `/simplify`, `/simplify focus on duplication` |
| `/qc-helper` | Answer questions about Qwen Code usage and configuration | `/qc-helper how do I configure MCP?` |
| Command | Description | Usage Examples |
| ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `/review` | Multi-agent code review (12 parallel agents at high effort) | `/review`, `/review 123`, `/review 123 --comment`, `/review --effort low` |
| `/coordinate` | Coordinate read-only workers and one optional worktree writer | `/coordinate investigate and fix the authentication regression` |
| `/loop` | Run a prompt on a recurring schedule | `/loop 5m check the build` |
| `/simplify` | Review recent changes and apply safe cleanup edits directly | `/simplify`, `/simplify focus on duplication` |
| `/qc-helper` | Answer questions about Qwen Code usage and configuration | `/qc-helper how do I configure MCP?` |

See [Code Review](./code-review.md) for full `/review` documentation.

Expand Down
30 changes: 30 additions & 0 deletions docs/users/features/multi-agent-coordination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Multi-Agent Coordination

Qwen Code can coordinate several teammates with the experimental Agent Team runtime. Teammates receive separate tasks, share a task list, exchange messages, and appear in the existing Agent View tabs. `/coordinate` defaults investigation workers to an enforced read-only tool set and can place one writer in a leader-owned Git worktree.

## Enable Agent Team

Set `experimental.agentTeam` to `true` in Qwen Code settings and restart, or start Qwen Code with `QWEN_CODE_ENABLE_AGENT_TEAM=1`.

## Run a coordinated task

Use the bundled skill with a goal:

```text
/coordinate investigate the authentication regression and propose the smallest fix
```

The leader creates a team, assigns up to three independent workstreams, and uses the existing team tools for messages and task state. Teammate conversations and approvals remain visible through the existing Agent View UI. Read-only teammates cannot execute shell commands or write files. If implementation is needed, the leader can create one Git worktree and pin one writer teammate to it; the leader remains the only merge authority for the current branch.

If Agent Team is disabled, `/coordinate` can still use ordinary foreground agents for read-only parallel investigation. That fallback is delegation, not a collaborating team: the workers report only to the leader.

## Choosing the right multi-agent mode

| Mode | Use it for | Communication | Workspace behavior |
| ----------------------------- | --------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
| `/coordinate` with Agent Team | Different workstreams contributing to one result | Shared tasks and teammate messages | Enforced read-only workers; optional single worktree writer |
| Subagents | Small delegated tasks | Worker reports to parent | Depends on the selected agent |
| Arena | Several models competing on the same task | Agents do not collaborate | Isolated worktrees; one winner is selected |
| Herdr | Coordinating different CLI products or remote terminal sessions | External terminal-level control | Managed outside Qwen Code |
Comment thread
yiliang114 marked this conversation as resolved.

The current workflow deliberately reuses the in-process Agent Team runtime and Agent View UI. Teammates normally inherit the session model, although an agent definition can override it. Persistent independent PTY sessions, cross-vendor workers, and remote attach are separate product concerns and are not implemented by `/coordinate`.
10 changes: 7 additions & 3 deletions packages/core/src/agents/runtime/subagent-plan-tool-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@ export const SUBAGENT_PLAN_LIFECYCLE_TOOLS: ReadonlySet<string> = new Set([
ToolNames.EXIT_PLAN_MODE,
]);

const PLAN_REQUIRED_TEAMMATE_PRE_APPROVAL_TOOLS: ReadonlySet<string> = new Set([
ToolNames.EXIT_PLAN_MODE,
export const READ_ONLY_INSPECTION_TOOLS: readonly string[] = [
ToolNames.READ_FILE,
ToolNames.GREP,
ToolNames.GLOB,
ToolNames.LS,
ToolNames.LSP,
ToolNames.TASK_LIST,
ToolNames.TOOL_SEARCH,
Comment thread
yiliang114 marked this conversation as resolved.
ToolNames.READ_MCP_RESOURCE,
];

const PLAN_REQUIRED_TEAMMATE_PRE_APPROVAL_TOOLS: ReadonlySet<string> = new Set([
ToolNames.EXIT_PLAN_MODE,
ToolNames.TASK_LIST,
...READ_ONLY_INSPECTION_TOOLS,
]);

const PRE_APPROVAL_TASK_CLAIM_KEYS: ReadonlySet<string> = new Set([
Expand Down
89 changes: 83 additions & 6 deletions packages/core/src/agents/team/TeamManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { PermissionMode } from '../../hooks/types.js';
import { AgentStatus, isTerminalStatus } from '../runtime/agent-types.js';
import { AgentEventType } from '../runtime/agent-events.js';
import type {
AgentRoundTextEvent,
AgentStatusChangeEvent,
AgentToolCallEvent,
AgentToolResultEvent,
Expand Down Expand Up @@ -75,6 +76,8 @@ import { runWithTeammateIdentity } from './identity.js';
import type { SubagentManager } from '../../subagents/subagent-manager.js';
import type { ToolConfig } from '../runtime/agent-types.js';
import { runOutsideAgentContext } from '../runtime/agent-context.js';
import { READ_ONLY_INSPECTION_TOOLS } from '../runtime/subagent-plan-tool-policy.js';
import { ToolNames } from '../../tools/tool-names.js';

const debug = createDebugLogger('AGENTS_TEAM_MANAGER');

Expand All @@ -98,6 +101,8 @@ export interface TeammateSpawnConfig {
cwd?: string;
/** Start this teammate in plan mode and require leader plan approval. */
planModeRequired?: boolean;
/** Restrict this teammate to read-only inspection and team coordination. */
readOnly?: boolean;
}

export interface TeamPlanApprovalRequest {
Expand Down Expand Up @@ -188,9 +193,15 @@ export class TeamManager {
* agentId so we can release each agent's listeners as soon as
* it reaches a terminal status — not just at full team
* cleanup. Otherwise long-running sessions accumulate dead
* listeners (4 per spawn) on shared emitters. */
* listeners (5 per spawn) on shared emitters. */
private readonly eventBridgeCleanups = new Map<string, () => void>();

/** Last model-visible answer from each teammate's active turn. */
private readonly pendingFinalReports = new Map<string, string>();

/** Teammates that explicitly reported to the leader during this turn. */
private readonly explicitLeaderReports = new Set<string>();
Comment thread
yiliang114 marked this conversation as resolved.

/** Unsubscribe from task update notifications. */
private taskUpdateUnsubscribe?: () => void;

Expand Down Expand Up @@ -293,6 +304,7 @@ export class TeamManager {
isActive: undefined,
subscriptions: [],
planModeRequired: config.planModeRequired || undefined,
readOnly: config.readOnly || undefined,
mode: config.planModeRequired ? PermissionMode.Plan : undefined,
};

Expand Down Expand Up @@ -321,6 +333,8 @@ export class TeamManager {
const idx = this.teamFile.members.indexOf(member);
if (idx !== -1) this.teamFile.members.splice(idx, 1);
this.pendingMessages.delete(agentId);
this.pendingFinalReports.delete(agentId);
this.explicitLeaderReports.delete(agentId);
Comment thread
yiliang114 marked this conversation as resolved.
this.lastActivityAt.delete(agentId);
this.agentIdentities.delete(agentId);
if (eventBridgeAttached) {
Expand Down Expand Up @@ -394,12 +408,28 @@ export class TeamManager {
}
}

if (config.readOnly) {
const tools = [
...READ_ONLY_INSPECTION_TOOLS,
ToolNames.SEND_MESSAGE,
ToolNames.TASK_LIST,
ToolNames.TASK_UPDATE,
];
toolConfig = {
tools: [...tools],
executionAllowedTools: [...tools],
};
}

// Build system prompt: subagent prompt (if any) or user prompt + team addendum.
const addendum = buildTeammatePromptAddendum(
name,
this.teamFile.name,
LEADER_NAME,
{ planModeRequired: config.planModeRequired },
{
planModeRequired: config.planModeRequired,
readOnly: config.readOnly,
},
);
const basePrompt = subagentPrompt ?? config.prompt;
const systemPrompt = basePrompt
Expand Down Expand Up @@ -506,6 +536,7 @@ export class TeamManager {
message: string,
from?: string,
summary?: string,
automatic = false,
): Promise<void> {
// Messages addressed to the leader go to leader's mailbox.
if (
Expand All @@ -525,7 +556,7 @@ export class TeamManager {
? findMemberByName(this.teamFile.members, from)
: undefined;
const shutdownResponse =
sender && this._shutdownPending.has(sender.name)
sender && !automatic && this._shutdownPending.has(sender.name)
? classifyShutdownResponse(message)
: undefined;

Expand All @@ -537,6 +568,9 @@ export class TeamManager {
read: false,
type: shutdownResponse,
});
if (sender && !automatic) {
this.explicitLeaderReports.add(sender.agentId);
}
this.teamEventEmitter.emit(TeamEventType.MESSAGE_SENT, {
from: from ?? 'unknown',
to: LEADER_NAME,
Expand Down Expand Up @@ -1360,6 +1394,8 @@ export class TeamManager {
}

this.pendingMessages.clear();
this.pendingFinalReports.clear();
this.explicitLeaderReports.clear();
Comment thread
yiliang114 marked this conversation as resolved.
this.lastActivityAt.clear();
this.agentIdentities.clear();
this.teamEventEmitter.removeAllListeners();
Expand Down Expand Up @@ -1403,6 +1439,11 @@ export class TeamManager {
const onStatusChange = (event: AgentStatusChangeEvent) => {
recordActivity();

if (event.newStatus === AgentStatus.RUNNING) {
this.pendingFinalReports.delete(agentId);
this.explicitLeaderReports.delete(agentId);
}
Comment thread
yiliang114 marked this conversation as resolved.

this.teamEventEmitter.emit(TeamEventType.TEAMMATE_STATUS_CHANGE, {
agentId,
name: agentName,
Expand All @@ -1412,6 +1453,25 @@ export class TeamManager {
});

if (event.newStatus === AgentStatus.IDLE) {
const finalReport = this.pendingFinalReports.get(agentId);
const explicitlyReported = this.explicitLeaderReports.has(agentId);
this.pendingFinalReports.delete(agentId);
Comment thread
yiliang114 marked this conversation as resolved.
this.explicitLeaderReports.delete(agentId);
Comment thread
yiliang114 marked this conversation as resolved.

if (!explicitlyReported && !event.roundCancelledByUser) {
Comment thread
yiliang114 marked this conversation as resolved.
this.fireAndForget(
`reportFinalAnswer(${agentId})`,
Comment thread
yiliang114 marked this conversation as resolved.
this.sendMessage(
LEADER_NAME,
finalReport ??
`${agentName} completed a turn without a model-visible final answer. Check the shared task list or send a follow-up if more detail is needed.`,
agentName,
`${agentName} completed a turn`,
true,
),
);
}

this.teamEventEmitter.emit(TeamEventType.TEAMMATE_IDLE, {
agentId,
name: agentName,
Expand Down Expand Up @@ -1463,6 +1523,8 @@ export class TeamManager {
// lost — better to refuse the send (handled at sendMessage
// by the missing entry) than accept it and drop it.
this.pendingMessages.delete(agentId);
this.pendingFinalReports.delete(agentId);
this.explicitLeaderReports.delete(agentId);
Comment thread
yiliang114 marked this conversation as resolved.
this.lastActivityAt.delete(agentId);
this.agentIdentities.delete(agentId);
this._shutdownPending.delete(agentName);
Expand All @@ -1483,7 +1545,18 @@ export class TeamManager {
recordActivity();
};

const onRoundText = (event: AgentRoundTextEvent) => {
recordActivity();
const text = event.text.trim();
this.pendingFinalReports.delete(agentId);
if (text) {
this.pendingFinalReports.set(agentId, text);
this.explicitLeaderReports.delete(agentId);
Comment thread
yiliang114 marked this conversation as resolved.
}
};

emitter.on(AgentEventType.STATUS_CHANGE, onStatusChange);
emitter.on(AgentEventType.ROUND_TEXT, onRoundText);
emitter.on(AgentEventType.TOOL_CALL, onToolCall);
emitter.on(AgentEventType.TOOL_RESULT, onToolResult);

Expand Down Expand Up @@ -1513,6 +1586,7 @@ export class TeamManager {
// release this agent's listeners on terminal status.
this.eventBridgeCleanups.set(agentId, () => {
emitter.off(AgentEventType.STATUS_CHANGE, onStatusChange);
emitter.off(AgentEventType.ROUND_TEXT, onRoundText);
emitter.off(AgentEventType.TOOL_CALL, onToolCall);
emitter.off(AgentEventType.TOOL_RESULT, onToolResult);
emitter.off(AgentEventType.TOOL_WAITING_APPROVAL, onApproval);
Expand Down Expand Up @@ -1671,6 +1745,8 @@ export class TeamManager {
const agent = this.getAgentFromBackend(agentId);
if (!agent) return;
if (agent.getStatus() !== AgentStatus.IDLE) return;
if (findMemberByName(this.teamFile.members, agentName)?.readOnly) return;
if (this._shutdownPending.has(agentName)) return;

const pendingTasks =
pending ??
Expand All @@ -1683,6 +1759,7 @@ export class TeamManager {
for (const task of pendingTasks) {
if (task.owner) continue;
if (task.blockedBy.length > 0) continue;
if (this._shutdownPending.has(agentName)) return;

const claimed = await claimTask(this.teamFile.name, task.id, agentId, {
checkAgentBusy: true,
Expand Down Expand Up @@ -1736,10 +1813,10 @@ export class TeamManager {
const agent = this.getAgentFromBackend(member.agentId);
if (!agent) return false;
if (agent.getStatus() !== AgentStatus.IDLE) return false;
if (member.readOnly) return false;
// Don't auto-claim a task for a teammate the leader is shutting
// down — it would start work it's about to abandon. flushNextMessage
// gates its own auto-claim on the same set; this is the task-update
// -triggered path, which reaches tryAutoClaimTask directly.
// down — it would start work it's about to abandon. tryAutoClaimTask
// repeats this check after async task reads for both claim paths.
if (this._shutdownPending.has(member.name)) return false;
const queue = this.pendingMessages.get(member.agentId) ?? [];
return queue.length === 0;
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/agents/team/promptAddendum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,15 @@ describe('buildTeammatePromptAddendum', () => {
expect(prompt).toContain('call exit_plan_mode');
expect(prompt).toContain('Do not use send_message for plan approval');
});

it('marks read-only tasks complete before the turn-ending report', () => {
const prompt = buildTeammatePromptAddendum('reader', 'team', 'leader', {
readOnly: true,
});

expect(prompt).toContain('MARK COMPLETE');
expect(prompt.indexOf('MARK COMPLETE')).toBeLessThan(
prompt.indexOf('REPORT RESULTS'),
);
Comment thread
yiliang114 marked this conversation as resolved.
});
});
Loading
Loading