mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-22 15:15:18 +00:00
fix(core): ignore agent names without active teams (#5115)
This commit is contained in:
parent
75e8f259c4
commit
4748705bc8
2 changed files with 63 additions and 32 deletions
|
|
@ -338,6 +338,35 @@ describe('AgentTool', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('does not expose teammate name when teams are disabled', () => {
|
||||
const schema = agentTool.schema;
|
||||
const parameters = schema.parametersJsonSchema as {
|
||||
properties: {
|
||||
name?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
expect(parameters.properties.name).toBeUndefined();
|
||||
});
|
||||
|
||||
it('exposes teammate name when teams are enabled', async () => {
|
||||
vi.mocked(config.isAgentTeamEnabled).mockReturnValue(true);
|
||||
|
||||
const teamAgentTool = new AgentTool(config);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const schema = teamAgentTool.schema;
|
||||
const parameters = schema.parametersJsonSchema as {
|
||||
properties: {
|
||||
name?: {
|
||||
description?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
expect(parameters.properties.name?.description).toContain('active team');
|
||||
});
|
||||
|
||||
it('should generate schema without enum when no subagents available', async () => {
|
||||
vi.mocked(mockSubagentManager.listSubagents).mockResolvedValue([]);
|
||||
|
||||
|
|
@ -519,8 +548,10 @@ describe('AgentTool', () => {
|
|||
});
|
||||
|
||||
describe('team routing', () => {
|
||||
it('rejects `name` when no team is active', async () => {
|
||||
it('falls back to one-shot when `name` is supplied without a team', async () => {
|
||||
vi.mocked(config.getTeamManager).mockReturnValue(null);
|
||||
vi.mocked(mockSubagentManager.loadSubagent).mockResolvedValue(null);
|
||||
|
||||
const invocation = agentTool.build({
|
||||
description: 'Spawn helper',
|
||||
prompt: 'Do work',
|
||||
|
|
@ -528,11 +559,11 @@ describe('AgentTool', () => {
|
|||
name: 'helper',
|
||||
});
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
expect(result.error).toBeDefined();
|
||||
expect(result.llmContent).toContain('no active team');
|
||||
expect(result.llmContent).toContain('"helper"');
|
||||
// Subagent must NOT have been spawned as a one-shot fallback.
|
||||
expect(mockSubagentManager.loadSubagent).not.toHaveBeenCalled();
|
||||
|
||||
expect(result.llmContent).not.toContain('no active team');
|
||||
expect(mockSubagentManager.loadSubagent).toHaveBeenCalledWith(
|
||||
'file-search',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -203,6 +203,13 @@ export interface AgentParams {
|
|||
|
||||
const debugLogger = createDebugLogger('AGENT');
|
||||
|
||||
const TEAM_AGENT_NAME_PROPERTY = {
|
||||
type: 'string',
|
||||
description:
|
||||
'When provided, spawn as a named teammate via the active team ' +
|
||||
'instead of a one-shot subagent. Requires an active team context.',
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps ApprovalMode to PermissionMode for hook events.
|
||||
*/
|
||||
|
|
@ -540,12 +547,9 @@ export class AgentTool extends BaseDeclarativeTool<AgentParams, ToolResult> {
|
|||
description:
|
||||
'Set to true to run this agent in the background. You will be notified when it completes.',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description:
|
||||
'When provided, spawn as a named teammate via the active team ' +
|
||||
'instead of a one-shot subagent. Requires an active team context.',
|
||||
},
|
||||
...(config.isAgentTeamEnabled()
|
||||
? { name: TEAM_AGENT_NAME_PROPERTY }
|
||||
: {}),
|
||||
isolation: {
|
||||
type: 'string',
|
||||
enum: ['worktree'],
|
||||
|
|
@ -725,6 +729,7 @@ assistant: Uses the ${ToolNames.AGENT} tool to launch the test-runner agent
|
|||
subagent_type?: {
|
||||
enum?: string[];
|
||||
};
|
||||
name?: typeof TEAM_AGENT_NAME_PROPERTY;
|
||||
};
|
||||
};
|
||||
if (schema.properties && schema.properties.subagent_type) {
|
||||
|
|
@ -734,6 +739,13 @@ assistant: Uses the ${ToolNames.AGENT} tool to launch the test-runner agent
|
|||
delete schema.properties.subagent_type.enum;
|
||||
}
|
||||
}
|
||||
if (schema.properties) {
|
||||
if (this.config.isAgentTeamEnabled()) {
|
||||
schema.properties.name = TEAM_AGENT_NAME_PROPERTY;
|
||||
} else {
|
||||
delete schema.properties.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override validateToolParams(params: AgentParams): string | null {
|
||||
|
|
@ -1630,29 +1642,17 @@ class AgentToolInvocation extends BaseToolInvocation<AgentParams, ToolResult> {
|
|||
updateOutput?: (output: ToolResultDisplay) => void,
|
||||
): Promise<ToolResult> {
|
||||
// ─── Team routing ────────────────────────────────────
|
||||
// When a team is active AND the caller passed an explicit
|
||||
// `name`, route through TeamManager as a named teammate.
|
||||
// Without a name we fall through to the regular one-shot
|
||||
// subagent flow — the schema says "When provided, spawn as
|
||||
// a named teammate," so the absence of `name` means the
|
||||
// caller wants a one-shot, not a teammate.
|
||||
// A name only means "spawn a teammate" while a team is active. Older
|
||||
// prompts may still pass it without a team; treat that as a normal
|
||||
// one-shot agent instead of failing the whole task.
|
||||
if (this.params.name && !isTeammate()) {
|
||||
// The schema for `name` says it requires an active team,
|
||||
// so reject the call up front instead of silently launching
|
||||
// a different kind of agent (one-shot subagent) that
|
||||
// ignores the supplied name.
|
||||
if (!this.config.getTeamManager()) {
|
||||
const msg =
|
||||
`Cannot spawn teammate "${this.params.name}": no active team. ` +
|
||||
`Use team_create to start a team first, or omit "name" to ` +
|
||||
`launch a one-shot subagent.`;
|
||||
return {
|
||||
llmContent: msg,
|
||||
returnDisplay: msg,
|
||||
error: { message: msg },
|
||||
};
|
||||
debugLogger.debug(
|
||||
`[AgentTool] Ignoring teammate name "${this.params.name}" because no team is active.`,
|
||||
);
|
||||
} else {
|
||||
return this.executeTeammate(this.params.name, signal, updateOutput);
|
||||
}
|
||||
return this.executeTeammate(this.params.name, signal, updateOutput);
|
||||
}
|
||||
|
||||
// ── Isolation state hoisted to the outermost scope ────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue