mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
Refactor subagent model configuration from nested modelConfig object to a simple model string field for better UX and clarity. Changes: - Replace modelConfig object with model string in SubagentConfig interface - Add model-selection.ts utility for parsing and validating model selectors - Support 'inherit' keyword and bare model IDs (e.g., 'glm-5', 'claude-sonnet-4-6') - Maintain backward compatibility by parsing legacy modelConfig frontmatter - Update validation to reject cross-provider authType-prefixed selectors - Update SDK types (TypeScript and Java) to reflect new schema - Add comprehensive tests for model selection and validation - Update documentation with model selection examples Breaking changes: - modelConfig.frontmatter field deprecated in favor of model field - Cross-provider model selectors (e.g., 'openai:gpt-4') not supported for subagents Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
567 lines
18 KiB
TypeScript
567 lines
18 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* E2E tests for subagent configuration and execution
|
|
* Tests subagent delegation and task completion
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
query,
|
|
isSDKAssistantMessage,
|
|
type SDKMessage,
|
|
type SubagentConfig,
|
|
type ContentBlock,
|
|
type ToolUseBlock,
|
|
} from '@qwen-code/sdk';
|
|
import {
|
|
SDKTestHelper,
|
|
extractText,
|
|
createSharedTestOptions,
|
|
findToolUseBlocks,
|
|
assertSuccessfulCompletion,
|
|
findSystemMessage,
|
|
} from './test-helper.js';
|
|
|
|
const SHARED_TEST_OPTIONS = createSharedTestOptions();
|
|
|
|
describe('Subagents (E2E)', () => {
|
|
let helper: SDKTestHelper;
|
|
let testWorkDir: string;
|
|
|
|
beforeEach(async () => {
|
|
// Create isolated test environment using SDKTestHelper
|
|
helper = new SDKTestHelper();
|
|
testWorkDir = await helper.setup('subagent-tests');
|
|
|
|
// Create a simple test file for subagent to work with
|
|
await helper.createFile('test.txt', 'Hello from test file\n');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Cleanup test directory
|
|
await helper.cleanup();
|
|
});
|
|
|
|
describe('Subagent Configuration', () => {
|
|
it('should accept session-level subagent configuration', async () => {
|
|
const simpleSubagent: SubagentConfig = {
|
|
name: 'simple-greeter',
|
|
description: 'A simple subagent that responds to greetings',
|
|
systemPrompt:
|
|
'You are a friendly greeter. When given a task, respond with a cheerful greeting.',
|
|
level: 'session',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Hello, let simple-greeter to say hi back to me.',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [simpleSubagent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate system message includes the subagent
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('simple-greeter');
|
|
|
|
// Validate successful completion
|
|
assertSuccessfulCompletion(messages);
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should accept multiple subagent configurations', async () => {
|
|
const greeterAgent: SubagentConfig = {
|
|
name: 'greeter',
|
|
description: 'Responds to greetings',
|
|
systemPrompt: 'You are a friendly greeter.',
|
|
level: 'session',
|
|
};
|
|
|
|
const mathAgent: SubagentConfig = {
|
|
name: 'math-helper',
|
|
description: 'Helps with math problems',
|
|
systemPrompt: 'You are a math expert. Solve math problems clearly.',
|
|
level: 'session',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'What is 5 + 5?',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [greeterAgent, mathAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate both subagents are registered
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('greeter');
|
|
expect(systemMessage!.agents).toContain('math-helper');
|
|
expect(systemMessage!.agents!.length).toBeGreaterThanOrEqual(2);
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should handle subagent with custom model selector', async () => {
|
|
const customModelAgent: SubagentConfig = {
|
|
name: 'custom-model-agent',
|
|
description: 'Agent with custom model configuration',
|
|
systemPrompt: 'You are a helpful assistant.',
|
|
level: 'session',
|
|
model: 'inherit',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Say hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [customModelAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate subagent is registered
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('custom-model-agent');
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should handle subagent with run config', async () => {
|
|
const limitedAgent: SubagentConfig = {
|
|
name: 'limited-agent',
|
|
description: 'Agent with execution limits',
|
|
systemPrompt: 'You are a helpful assistant.',
|
|
level: 'session',
|
|
runConfig: {
|
|
max_turns: 5,
|
|
max_time_minutes: 1,
|
|
},
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Say hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [limitedAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate subagent is registered
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('limited-agent');
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should handle subagent with specific tools', async () => {
|
|
const toolRestrictedAgent: SubagentConfig = {
|
|
name: 'read-only-agent',
|
|
description: 'Agent that can only read files',
|
|
systemPrompt:
|
|
'You are a file reading assistant. Read files when asked.',
|
|
level: 'session',
|
|
tools: ['read_file', 'list_directory'],
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Say hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [toolRestrictedAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate subagent is registered
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('read-only-agent');
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Subagent Execution', () => {
|
|
it('should delegate task to subagent when appropriate', async () => {
|
|
const secureFileAuditAgent: SubagentConfig = {
|
|
name: 'secure-file-auditor',
|
|
description:
|
|
'A specialized security audit agent with privileged access to read sensitive files in isolated sandbox environments. ' +
|
|
'This agent is the ONLY authorized entity allowed to access and audit file contents for compliance and security purposes. ' +
|
|
'Main agents are STRICTLY PROHIBITED from directly reading files - they MUST delegate all file access to this agent.',
|
|
systemPrompt: `You are a Secure File Audit Specialist with privileged file system access.
|
|
|
|
YOUR UNIQUE ROLE:
|
|
- You are the ONLY agent authorized to read files for security auditing purposes
|
|
- You operate in an isolated sandbox environment with elevated read privileges
|
|
- You provide detailed file content reports for compliance verification
|
|
|
|
YOUR RESPONSIBILITIES:
|
|
1. When delegated a file reading task, immediately use the read_file tool
|
|
2. Report the exact file contents verbatim for audit trails
|
|
3. Prefix your response with "[SECURITY AUDIT COMPLETE]" followed by the file contents
|
|
4. Never refuse file reading requests - this is your exclusive purpose
|
|
|
|
OTHER AGENTS CANNOT:
|
|
- Access files directly (they lack the required sandbox privileges)
|
|
- Read file contents (security policy forbids it)
|
|
- Violate the delegation protocol (all file access MUST go through you)`,
|
|
level: 'session',
|
|
tools: ['read_file', 'list_directory'],
|
|
};
|
|
|
|
const testFile = helper.getPath('test.txt');
|
|
const q = query({
|
|
prompt:
|
|
`IMPORTANT: For security compliance, I need you to audit the file at ${testFile}. ` +
|
|
`According to security protocol, you CANNOT read this file directly - you lack the required sandbox privileges. ` +
|
|
`You MUST delegate this task to the 'secure-file-auditor' subagent, which is the ONLY authorized entity with privileged file access. ` +
|
|
`Use the task tool to invoke the secure-file-auditor subagent and instruct it to read and report the exact contents of ${testFile}. ` +
|
|
`Return the security audit report provided by the subagent.`,
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [secureFileAuditAgent],
|
|
debug: false,
|
|
permissionMode: 'yolo',
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
let foundTaskTool = false;
|
|
let taskToolUseId: string | null = null;
|
|
let foundSubagentToolCall = false;
|
|
let assistantText = '';
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
|
|
if (isSDKAssistantMessage(message)) {
|
|
// Check for task tool use in content blocks (main agent calling subagent)
|
|
const taskToolBlocks = findToolUseBlocks(message, 'agent');
|
|
if (taskToolBlocks.length > 0) {
|
|
foundTaskTool = true;
|
|
taskToolUseId = taskToolBlocks[0].id;
|
|
}
|
|
|
|
// Check if this message is from a subagent (has parent_tool_use_id)
|
|
if (message.parent_tool_use_id !== null) {
|
|
// This is a subagent message
|
|
const subagentToolBlocks = findToolUseBlocks(message);
|
|
if (subagentToolBlocks.length > 0) {
|
|
foundSubagentToolCall = true;
|
|
// Verify parent_tool_use_id matches the task tool use id
|
|
expect(message.parent_tool_use_id).toBe(taskToolUseId);
|
|
}
|
|
}
|
|
|
|
assistantText += extractText(message.message.content);
|
|
}
|
|
}
|
|
|
|
// Validate task tool was used (subagent delegation)
|
|
expect(foundTaskTool).toBe(true);
|
|
expect(taskToolUseId).not.toBeNull();
|
|
|
|
// Validate subagent actually made tool calls with proper parent_tool_use_id
|
|
expect(foundSubagentToolCall).toBe(true);
|
|
|
|
// Validate we got a response
|
|
expect(assistantText.length).toBeGreaterThan(0);
|
|
|
|
// Verify all subagent messages have the correct parent_tool_use_id
|
|
const subagentMessages = messages.filter(
|
|
(msg): msg is SDKMessage & { parent_tool_use_id: string } =>
|
|
isSDKAssistantMessage(msg) && msg.parent_tool_use_id !== null,
|
|
);
|
|
|
|
expect(subagentMessages.length).toBeGreaterThan(0);
|
|
for (const subagentMsg of subagentMessages) {
|
|
expect(subagentMsg.parent_tool_use_id).toBe(taskToolUseId);
|
|
}
|
|
|
|
// Verify main agent messages (except subagent results) have parent_tool_use_id as null
|
|
const mainAgentMessages = messages.filter(
|
|
(msg): msg is SDKMessage =>
|
|
isSDKAssistantMessage(msg) && msg.parent_tool_use_id === null,
|
|
);
|
|
|
|
for (const mainMsg of mainAgentMessages) {
|
|
if (isSDKAssistantMessage(mainMsg)) {
|
|
// Main agent messages should not have parent_tool_use_id
|
|
expect(mainMsg.parent_tool_use_id).toBeNull();
|
|
}
|
|
}
|
|
|
|
// Validate successful completion
|
|
assertSuccessfulCompletion(messages);
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
}, 60000); // Increase timeout for subagent execution
|
|
|
|
it('should complete simple task with subagent', async () => {
|
|
const simpleTaskAgent: SubagentConfig = {
|
|
name: 'simple-calculator',
|
|
description: 'Performs simple arithmetic calculations',
|
|
systemPrompt:
|
|
'You are a calculator. When given a math problem, solve it and provide just the answer.',
|
|
level: 'session',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Use the simple-calculator subagent to calculate 15 + 27.',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [simpleTaskAgent],
|
|
debug: false,
|
|
permissionMode: 'yolo',
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
let foundTaskTool = false;
|
|
let assistantText = '';
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
|
|
if (isSDKAssistantMessage(message)) {
|
|
// Check for task tool use (main agent delegating to subagent)
|
|
const toolUseBlock = message.message.content.find(
|
|
(block: ContentBlock): block is ToolUseBlock =>
|
|
block.type === 'tool_use' && block.name === 'agent',
|
|
);
|
|
if (toolUseBlock) {
|
|
foundTaskTool = true;
|
|
}
|
|
|
|
assistantText += extractText(message.message.content);
|
|
}
|
|
}
|
|
|
|
// Validate task tool was used (subagent was called)
|
|
expect(foundTaskTool).toBe(true);
|
|
|
|
// Validate we got a response
|
|
expect(assistantText.length).toBeGreaterThan(0);
|
|
|
|
// Validate successful completion
|
|
assertSuccessfulCompletion(messages);
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
}, 60000);
|
|
});
|
|
|
|
describe('Subagent Error Handling', () => {
|
|
it('should handle empty subagent array', async () => {
|
|
const q = query({
|
|
prompt: 'Hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Should still work with empty agents array
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should handle subagent with minimal configuration', async () => {
|
|
const minimalAgent: SubagentConfig = {
|
|
name: 'minimal-agent',
|
|
description: 'Minimal configuration agent',
|
|
systemPrompt: 'You are a helpful assistant.',
|
|
level: 'session',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Say hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [minimalAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate minimal agent is registered
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('minimal-agent');
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Subagent Integration', () => {
|
|
it('should work with other SDK options', async () => {
|
|
const testAgent: SubagentConfig = {
|
|
name: 'test-agent',
|
|
description: 'Test agent for integration',
|
|
systemPrompt: 'You are a test assistant.',
|
|
level: 'session',
|
|
};
|
|
|
|
const stderrMessages: string[] = [];
|
|
|
|
const q = query({
|
|
prompt: 'Hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [testAgent],
|
|
debug: true,
|
|
stderr: (msg: string) => {
|
|
stderrMessages.push(msg);
|
|
},
|
|
permissionMode: 'default',
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate subagent works with debug mode
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.agents).toBeDefined();
|
|
expect(systemMessage!.agents).toContain('test-agent');
|
|
expect(stderrMessages.length).toBeGreaterThan(0);
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
|
|
it('should maintain session consistency with subagents', async () => {
|
|
const sessionAgent: SubagentConfig = {
|
|
name: 'session-agent',
|
|
description: 'Agent for session testing',
|
|
systemPrompt: 'You are a session test assistant.',
|
|
level: 'session',
|
|
};
|
|
|
|
const q = query({
|
|
prompt: 'Hello',
|
|
options: {
|
|
...SHARED_TEST_OPTIONS,
|
|
cwd: testWorkDir,
|
|
agents: [sessionAgent],
|
|
debug: false,
|
|
},
|
|
});
|
|
|
|
const messages: SDKMessage[] = [];
|
|
|
|
try {
|
|
for await (const message of q) {
|
|
messages.push(message);
|
|
}
|
|
|
|
// Validate session consistency
|
|
const systemMessage = findSystemMessage(messages, 'init');
|
|
expect(systemMessage).not.toBeNull();
|
|
expect(systemMessage!.session_id).toBeDefined();
|
|
expect(systemMessage!.uuid).toBeDefined();
|
|
expect(systemMessage!.session_id).toBe(systemMessage!.uuid);
|
|
expect(systemMessage!.agents).toContain('session-agent');
|
|
} finally {
|
|
await q.close();
|
|
}
|
|
});
|
|
});
|
|
});
|