mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(core): Have subagents use a JSON schema type for input. (#17152)
This commit is contained in:
@@ -253,12 +253,16 @@ Body`);
|
|||||||
maxTimeMinutes: 5,
|
maxTimeMinutes: 5,
|
||||||
},
|
},
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
query: {
|
query: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
description: 'The task for the agent.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -309,13 +313,16 @@ Body`);
|
|||||||
displayName: undefined,
|
displayName: undefined,
|
||||||
agentCardUrl: 'https://example.com/card',
|
agentCardUrl: 'https://example.com/card',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
query: {
|
query: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The task for the agent.',
|
description: 'The task for the agent.',
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -247,13 +247,17 @@ export function markdownToAgentDefinition(
|
|||||||
markdown: FrontmatterAgentDefinition,
|
markdown: FrontmatterAgentDefinition,
|
||||||
): AgentDefinition {
|
): AgentDefinition {
|
||||||
const inputConfig = {
|
const inputConfig = {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
query: {
|
query: {
|
||||||
type: 'string' as const,
|
type: 'string',
|
||||||
description: 'The task for the agent.',
|
description: 'The task for the agent.',
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// query is not required because it defaults to "Get Started!" if not provided
|
||||||
|
required: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (markdown.kind === 'remote') {
|
if (markdown.kind === 'remote') {
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ describe('CliHelpAgent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have correctly configured inputs and outputs', () => {
|
it('should have correctly configured inputs and outputs', () => {
|
||||||
expect(localAgent.inputConfig.inputs['question']).toBeDefined();
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
expect(localAgent.inputConfig.inputs['question'].required).toBe(true);
|
const inputSchema = localAgent.inputConfig.inputSchema as any;
|
||||||
|
expect(inputSchema.properties['question']).toBeDefined();
|
||||||
|
expect(inputSchema.required).toContain('question');
|
||||||
|
|
||||||
expect(localAgent.outputConfig?.outputName).toBe('report');
|
expect(localAgent.outputConfig?.outputName).toBe('report');
|
||||||
expect(localAgent.outputConfig?.description).toBeDefined();
|
expect(localAgent.outputConfig?.description).toBeDefined();
|
||||||
|
|||||||
@@ -32,13 +32,16 @@ export const CliHelpAgent = (
|
|||||||
description:
|
description:
|
||||||
'Specialized in answering questions about how users use you, (Gemini CLI): features, documentation, and current runtime configuration.',
|
'Specialized in answering questions about how users use you, (Gemini CLI): features, documentation, and current runtime configuration.',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
question: {
|
question: {
|
||||||
description: 'The specific question about Gemini CLI.',
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
description: 'The specific question about Gemini CLI.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: ['question'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
outputConfig: {
|
outputConfig: {
|
||||||
outputName: 'report',
|
outputName: 'report',
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ describe('CodebaseInvestigatorAgent', () => {
|
|||||||
'Codebase Investigator Agent',
|
'Codebase Investigator Agent',
|
||||||
);
|
);
|
||||||
expect(CodebaseInvestigatorAgent.description).toBeDefined();
|
expect(CodebaseInvestigatorAgent.description).toBeDefined();
|
||||||
expect(
|
const inputSchema =
|
||||||
CodebaseInvestigatorAgent.inputConfig.inputs['objective'].required,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
).toBe(true);
|
CodebaseInvestigatorAgent.inputConfig.inputSchema as any;
|
||||||
|
expect(inputSchema.properties['objective']).toBeDefined();
|
||||||
|
expect(inputSchema.required).toContain('objective');
|
||||||
expect(CodebaseInvestigatorAgent.outputConfig?.outputName).toBe('report');
|
expect(CodebaseInvestigatorAgent.outputConfig?.outputName).toBe('report');
|
||||||
expect(CodebaseInvestigatorAgent.modelConfig?.model).toBe(
|
expect(CodebaseInvestigatorAgent.modelConfig?.model).toBe(
|
||||||
DEFAULT_GEMINI_MODEL,
|
DEFAULT_GEMINI_MODEL,
|
||||||
|
|||||||
@@ -51,14 +51,17 @@ export const CodebaseInvestigatorAgent: LocalAgentDefinition<
|
|||||||
Invoke this tool for tasks like vague requests, bug root-cause analysis, system refactoring, comprehensive feature implementation or to answer questions about the codebase that require investigation.
|
Invoke this tool for tasks like vague requests, bug root-cause analysis, system refactoring, comprehensive feature implementation or to answer questions about the codebase that require investigation.
|
||||||
It returns a structured report with key file paths, symbols, and actionable architectural insights.`,
|
It returns a structured report with key file paths, symbols, and actionable architectural insights.`,
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
objective: {
|
objective: {
|
||||||
|
type: 'string',
|
||||||
description: `A comprehensive and detailed description of the user's ultimate goal.
|
description: `A comprehensive and detailed description of the user's ultimate goal.
|
||||||
You must include original user's objective as well as questions and any extra context and questions you may have.`,
|
You must include original user's objective as well as questions and any extra context and questions you may have.`,
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: ['objective'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
outputConfig: {
|
outputConfig: {
|
||||||
outputName: 'report',
|
outputName: 'report',
|
||||||
|
|||||||
@@ -61,9 +61,13 @@ describe('DelegateToAgentTool', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
arg1: { type: 'string', description: 'Argument 1', required: true },
|
type: 'object',
|
||||||
arg2: { type: 'number', description: 'Argument 2', required: false },
|
properties: {
|
||||||
|
arg1: { type: 'string', description: 'Argument 1' },
|
||||||
|
arg2: { type: 'number', description: 'Argument 2' },
|
||||||
|
},
|
||||||
|
required: ['arg1'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runConfig: { maxTurns: 1, maxTimeMinutes: 1 },
|
runConfig: { maxTurns: 1, maxTimeMinutes: 1 },
|
||||||
@@ -76,8 +80,12 @@ describe('DelegateToAgentTool', () => {
|
|||||||
description: 'A remote agent',
|
description: 'A remote agent',
|
||||||
agentCardUrl: 'https://example.com/agent.json',
|
agentCardUrl: 'https://example.com/agent.json',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
query: { type: 'string', description: 'Query', required: true },
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
query: { type: 'string', description: 'Query' },
|
||||||
|
},
|
||||||
|
required: ['query'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -153,7 +161,7 @@ describe('DelegateToAgentTool', () => {
|
|||||||
await expect(() =>
|
await expect(() =>
|
||||||
invocation.execute(new AbortController().signal),
|
invocation.execute(new AbortController().signal),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
"arg1: Required. Expected inputs: 'arg1' (required string), 'arg2' (optional number).",
|
`Invalid arguments for agent 'test_agent': params must have required property 'arg1'. Input schema: ${JSON.stringify(mockAgentDef.inputConfig.inputSchema)}.`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -166,7 +174,7 @@ describe('DelegateToAgentTool', () => {
|
|||||||
await expect(() =>
|
await expect(() =>
|
||||||
invocation.execute(new AbortController().signal),
|
invocation.execute(new AbortController().signal),
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
"arg1: Expected string, received number. Expected inputs: 'arg1' (required string), 'arg2' (optional number).",
|
`Invalid arguments for agent 'test_agent': params/arg1 must be string. Input schema: ${JSON.stringify(mockAgentDef.inputConfig.inputSchema)}.`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,13 +195,16 @@ describe('DelegateToAgentTool', () => {
|
|||||||
...mockAgentDef,
|
...mockAgentDef,
|
||||||
name: 'invalid_agent',
|
name: 'invalid_agent',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
agent_name: {
|
agent_name: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Conflict',
|
description: 'Conflict',
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: ['agent_name'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,6 +216,37 @@ describe('DelegateToAgentTool', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow a remote agent missing a "query" input (will default at runtime)', () => {
|
||||||
|
const invalidRemoteAgentDef: AgentDefinition = {
|
||||||
|
kind: 'remote',
|
||||||
|
name: 'invalid_remote',
|
||||||
|
description: 'Conflict',
|
||||||
|
agentCardUrl: 'https://example.com/agent.json',
|
||||||
|
inputConfig: {
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
not_query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Not a query',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['not_query'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(registry as any).agents.set(
|
||||||
|
invalidRemoteAgentDef.name,
|
||||||
|
invalidRemoteAgentDef,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => new DelegateToAgentTool(registry, config, messageBus),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('should execute local agents silently without requesting confirmation', async () => {
|
it('should execute local agents silently without requesting confirmation', async () => {
|
||||||
const invocation = tool.build({
|
const invocation = tool.build({
|
||||||
agent_name: 'test_agent',
|
agent_name: 'test_agent',
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
||||||
import {
|
import {
|
||||||
BaseDeclarativeTool,
|
BaseDeclarativeTool,
|
||||||
Kind,
|
Kind,
|
||||||
@@ -21,6 +19,9 @@ import type { Config } from '../config/config.js';
|
|||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
import type { AgentDefinition, AgentInputs } from './types.js';
|
import type { AgentDefinition, AgentInputs } from './types.js';
|
||||||
import { SubagentToolWrapper } from './subagent-tool-wrapper.js';
|
import { SubagentToolWrapper } from './subagent-tool-wrapper.js';
|
||||||
|
import { SchemaValidator } from '../utils/schemaValidator.js';
|
||||||
|
import { type AnySchema } from 'ajv';
|
||||||
|
import { debugLogger } from '../utils/debugLogger.js';
|
||||||
|
|
||||||
export type DelegateParams = { agent_name: string } & Record<string, unknown>;
|
export type DelegateParams = { agent_name: string } & Record<string, unknown>;
|
||||||
|
|
||||||
@@ -35,82 +36,76 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<
|
|||||||
) {
|
) {
|
||||||
const definitions = registry.getAllDefinitions();
|
const definitions = registry.getAllDefinitions();
|
||||||
|
|
||||||
let schema: z.ZodTypeAny;
|
let toolSchema: AnySchema;
|
||||||
|
|
||||||
if (definitions.length === 0) {
|
if (definitions.length === 0) {
|
||||||
// Fallback if no agents are registered (mostly for testing/safety)
|
// Fallback if no agents are registered (mostly for testing/safety)
|
||||||
schema = z.object({
|
toolSchema = {
|
||||||
agent_name: z.string().describe('No agents are currently available.'),
|
type: 'object',
|
||||||
});
|
properties: {
|
||||||
|
agent_name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'No agents are currently available.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['agent_name'],
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const agentSchemas = definitions.map((def) => {
|
const agentSchemas = definitions.map((def) => {
|
||||||
const inputShape: Record<string, z.ZodTypeAny> = {
|
const schemaError = SchemaValidator.validateSchema(
|
||||||
agent_name: z.literal(def.name).describe(def.description),
|
def.inputConfig.inputSchema,
|
||||||
};
|
);
|
||||||
|
if (schemaError) {
|
||||||
|
throw new Error(`Invalid schema for ${def.name}: ${schemaError}`);
|
||||||
|
}
|
||||||
|
|
||||||
for (const [key, inputDef] of Object.entries(def.inputConfig.inputs)) {
|
const inputSchema = def.inputConfig.inputSchema;
|
||||||
if (key === 'agent_name') {
|
if (typeof inputSchema !== 'object' || inputSchema === null) {
|
||||||
|
throw new Error(`Agent '${def.name}' must provide an object schema.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaObj = inputSchema as Record<string, unknown>;
|
||||||
|
const properties = schemaObj['properties'] as
|
||||||
|
| Record<string, unknown>
|
||||||
|
| undefined;
|
||||||
|
if (properties && 'agent_name' in properties) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Agent '${def.name}' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.`,
|
`Agent '${def.name}' cannot have an input parameter named 'agent_name' as it is a reserved parameter for delegation.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let validator: z.ZodTypeAny;
|
if (def.kind === 'remote') {
|
||||||
|
if (!properties || !properties['query']) {
|
||||||
// Map input types to Zod
|
debugLogger.log(
|
||||||
switch (inputDef.type) {
|
'INFO',
|
||||||
case 'string':
|
`Remote agent '${def.name}' does not define a 'query' property in its inputSchema. It will default to 'Get Started!' during invocation.`,
|
||||||
validator = z.string();
|
);
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
validator = z.number();
|
|
||||||
break;
|
|
||||||
case 'boolean':
|
|
||||||
validator = z.boolean();
|
|
||||||
break;
|
|
||||||
case 'integer':
|
|
||||||
validator = z.number().int();
|
|
||||||
break;
|
|
||||||
case 'string[]':
|
|
||||||
validator = z.array(z.string());
|
|
||||||
break;
|
|
||||||
case 'number[]':
|
|
||||||
validator = z.array(z.number());
|
|
||||||
break;
|
|
||||||
default: {
|
|
||||||
// This provides compile-time exhaustiveness checking.
|
|
||||||
const _exhaustiveCheck: never = inputDef.type;
|
|
||||||
void _exhaustiveCheck;
|
|
||||||
throw new Error(`Unhandled agent input type: '${inputDef.type}'`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputDef.required) {
|
return {
|
||||||
validator = validator.optional();
|
type: 'object',
|
||||||
}
|
properties: {
|
||||||
|
agent_name: {
|
||||||
inputShape[key] = validator.describe(inputDef.description);
|
const: def.name,
|
||||||
}
|
description: def.description,
|
||||||
|
},
|
||||||
// Cast required because Zod can't infer the discriminator from dynamic keys
|
...(properties || {}),
|
||||||
return z.object(
|
},
|
||||||
inputShape,
|
required: [
|
||||||
) as z.ZodDiscriminatedUnionOption<'agent_name'>;
|
'agent_name',
|
||||||
|
...((schemaObj['required'] as string[]) || []),
|
||||||
|
],
|
||||||
|
} as AnySchema;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the discriminated union
|
// Create the anyOf schema
|
||||||
// z.discriminatedUnion requires at least 2 options, so we handle the single agent case
|
|
||||||
if (agentSchemas.length === 1) {
|
if (agentSchemas.length === 1) {
|
||||||
schema = agentSchemas[0];
|
toolSchema = agentSchemas[0];
|
||||||
} else {
|
} else {
|
||||||
schema = z.discriminatedUnion(
|
toolSchema = {
|
||||||
'agent_name',
|
anyOf: agentSchemas,
|
||||||
agentSchemas as [
|
};
|
||||||
z.ZodDiscriminatedUnionOption<'agent_name'>,
|
|
||||||
z.ZodDiscriminatedUnionOption<'agent_name'>,
|
|
||||||
...Array<z.ZodDiscriminatedUnionOption<'agent_name'>>,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +114,7 @@ export class DelegateToAgentTool extends BaseDeclarativeTool<
|
|||||||
'Delegate to Agent',
|
'Delegate to Agent',
|
||||||
registry.getToolDescription(),
|
registry.getToolDescription(),
|
||||||
Kind.Think,
|
Kind.Think,
|
||||||
zodToJsonSchema(schema),
|
toolSchema,
|
||||||
messageBus,
|
messageBus,
|
||||||
/* isOutputMarkdown */ true,
|
/* isOutputMarkdown */ true,
|
||||||
/* canUpdateOutput */ true,
|
/* canUpdateOutput */ true,
|
||||||
@@ -210,66 +205,17 @@ class DelegateInvocation extends BaseToolInvocation<
|
|||||||
|
|
||||||
const { agent_name: _agent_name, ...agentArgs } = this.params;
|
const { agent_name: _agent_name, ...agentArgs } = this.params;
|
||||||
|
|
||||||
// Validate specific agent arguments here using Zod to generate helpful error messages.
|
// Validate specific agent arguments here using SchemaValidator to generate helpful error messages.
|
||||||
const inputShape: Record<string, z.ZodTypeAny> = {};
|
const validationError = SchemaValidator.validate(
|
||||||
for (const [key, inputDef] of Object.entries(
|
definition.inputConfig.inputSchema,
|
||||||
definition.inputConfig.inputs,
|
agentArgs,
|
||||||
)) {
|
|
||||||
let validator: z.ZodTypeAny;
|
|
||||||
|
|
||||||
switch (inputDef.type) {
|
|
||||||
case 'string':
|
|
||||||
validator = z.string();
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
validator = z.number();
|
|
||||||
break;
|
|
||||||
case 'boolean':
|
|
||||||
validator = z.boolean();
|
|
||||||
break;
|
|
||||||
case 'integer':
|
|
||||||
validator = z.number().int();
|
|
||||||
break;
|
|
||||||
case 'string[]':
|
|
||||||
validator = z.array(z.string());
|
|
||||||
break;
|
|
||||||
case 'number[]':
|
|
||||||
validator = z.array(z.number());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
validator = z.unknown();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inputDef.required) {
|
|
||||||
validator = validator.optional();
|
|
||||||
}
|
|
||||||
|
|
||||||
inputShape[key] = validator.describe(inputDef.description);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentSchema = z.object(inputShape);
|
|
||||||
|
|
||||||
try {
|
|
||||||
agentSchema.parse(agentArgs);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof z.ZodError) {
|
|
||||||
const errorMessages = e.errors.map(
|
|
||||||
(err) => `${err.path.join('.')}: ${err.message}`,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const expectedInputs = Object.entries(definition.inputConfig.inputs)
|
if (validationError) {
|
||||||
.map(
|
|
||||||
([key, input]) =>
|
|
||||||
`'${key}' (${input.required ? 'required' : 'optional'} ${input.type})`,
|
|
||||||
)
|
|
||||||
.join(', ');
|
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${errorMessages.join(', ')}. Expected inputs: ${expectedInputs}.`,
|
`Invalid arguments for agent '${definition.name}': ${validationError}. Input schema: ${JSON.stringify(definition.inputConfig.inputSchema)}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invocation = this.buildSubInvocation(
|
const invocation = this.buildSubInvocation(
|
||||||
definition,
|
definition,
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ export const GeneralistAgent = (
|
|||||||
"A general-purpose AI agent with access to all tools. Use it for complex tasks that don't fit into other specialized agents.",
|
"A general-purpose AI agent with access to all tools. Use it for complex tasks that don't fit into other specialized agents.",
|
||||||
experimental: true,
|
experimental: true,
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
request: {
|
request: {
|
||||||
description: 'The task or question for the generalist agent.',
|
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
description: 'The task or question for the generalist agent.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: ['request'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
outputConfig: {
|
outputConfig: {
|
||||||
outputName: 'result',
|
outputName: 'result',
|
||||||
|
|||||||
@@ -223,7 +223,13 @@ const createTestDefinition = <TOutput extends z.ZodTypeAny = z.ZodUnknown>(
|
|||||||
name: 'TestAgent',
|
name: 'TestAgent',
|
||||||
description: 'An agent for testing.',
|
description: 'An agent for testing.',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: { goal: { type: 'string', required: true, description: 'goal' } },
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
goal: { type: 'string', description: 'goal' },
|
||||||
|
},
|
||||||
|
required: ['goal'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: 'gemini-test-model',
|
model: 'gemini-test-model',
|
||||||
@@ -411,8 +417,12 @@ describe('LocalAgentExecutor', () => {
|
|||||||
it('should log AgentFinish with error if run throws', async () => {
|
it('should log AgentFinish with error if run throws', async () => {
|
||||||
const definition = createTestDefinition();
|
const definition = createTestDefinition();
|
||||||
// Make the definition invalid to cause an error during run
|
// Make the definition invalid to cause an error during run
|
||||||
definition.inputConfig.inputs = {
|
definition.inputConfig.inputSchema = {
|
||||||
goal: { type: 'string', required: true, description: 'goal' },
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
goal: { type: 'string', description: 'goal' },
|
||||||
|
},
|
||||||
|
required: ['goal'],
|
||||||
};
|
};
|
||||||
const executor = await LocalAgentExecutor.create(
|
const executor = await LocalAgentExecutor.create(
|
||||||
definition,
|
definition,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import type {
|
|||||||
OutputObject,
|
OutputObject,
|
||||||
SubagentActivityEvent,
|
SubagentActivityEvent,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { AgentTerminateMode } from './types.js';
|
import { AgentTerminateMode, DEFAULT_QUERY_STRING } from './types.js';
|
||||||
import { templateString } from './utils.js';
|
import { templateString } from './utils.js';
|
||||||
import { DEFAULT_GEMINI_MODEL, isAutoModel } from '../config/models.js';
|
import { DEFAULT_GEMINI_MODEL, isAutoModel } from '../config/models.js';
|
||||||
import type { RoutingContext } from '../routing/routingStrategy.js';
|
import type { RoutingContext } from '../routing/routingStrategy.js';
|
||||||
@@ -395,7 +395,7 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
|||||||
chat = await this.createChatObject(augmentedInputs, tools);
|
chat = await this.createChatObject(augmentedInputs, tools);
|
||||||
const query = this.definition.promptConfig.query
|
const query = this.definition.promptConfig.query
|
||||||
? templateString(this.definition.promptConfig.query, augmentedInputs)
|
? templateString(this.definition.promptConfig.query, augmentedInputs)
|
||||||
: 'Get Started!';
|
: DEFAULT_QUERY_STRING;
|
||||||
let currentMessage: Content = { role: 'user', parts: [{ text: query }] };
|
let currentMessage: Content = { role: 'user', parts: [{ text: query }] };
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ const testDefinition: LocalAgentDefinition<z.ZodUnknown> = {
|
|||||||
name: 'MockAgent',
|
name: 'MockAgent',
|
||||||
description: 'A mock agent.',
|
description: 'A mock agent.',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
task: { type: 'string', required: true, description: 'task' },
|
type: 'object',
|
||||||
priority: { type: 'number', required: false, description: 'prio' },
|
properties: {
|
||||||
|
task: { type: 'string', description: 'task' },
|
||||||
|
priority: { type: 'number', description: 'prio' },
|
||||||
|
},
|
||||||
|
required: ['task'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const MOCK_AGENT_V1: AgentDefinition = {
|
|||||||
kind: 'local',
|
kind: 'local',
|
||||||
name: 'MockAgent',
|
name: 'MockAgent',
|
||||||
description: 'Mock Description V1',
|
description: 'Mock Description V1',
|
||||||
inputConfig: { inputs: {} },
|
inputConfig: { inputSchema: { type: 'object' } },
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: 'test',
|
model: 'test',
|
||||||
generateContentConfig: {
|
generateContentConfig: {
|
||||||
@@ -447,7 +447,7 @@ describe('AgentRegistry', () => {
|
|||||||
name: 'RemoteAgent',
|
name: 'RemoteAgent',
|
||||||
description: 'A remote agent',
|
description: 'A remote agent',
|
||||||
agentCardUrl: 'https://example.com/card',
|
agentCardUrl: 'https://example.com/card',
|
||||||
inputConfig: { inputs: {} },
|
inputConfig: { inputSchema: { type: 'object' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
|
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
|
||||||
@@ -470,7 +470,7 @@ describe('AgentRegistry', () => {
|
|||||||
name: 'RemoteAgent',
|
name: 'RemoteAgent',
|
||||||
description: 'A remote agent',
|
description: 'A remote agent',
|
||||||
agentCardUrl: 'https://example.com/card',
|
agentCardUrl: 'https://example.com/card',
|
||||||
inputConfig: { inputs: {} },
|
inputConfig: { inputSchema: { type: 'object' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
|
vi.mocked(A2AClientManager.getInstance).mockReturnValue({
|
||||||
@@ -791,7 +791,7 @@ describe('AgentRegistry', () => {
|
|||||||
name: 'RemoteAgent',
|
name: 'RemoteAgent',
|
||||||
description: 'A remote agent',
|
description: 'A remote agent',
|
||||||
agentCardUrl: 'https://example.com/card',
|
agentCardUrl: 'https://example.com/card',
|
||||||
inputConfig: { inputs: {} },
|
inputConfig: { inputSchema: { type: 'object' } },
|
||||||
};
|
};
|
||||||
|
|
||||||
await registry.testRegisterAgent(remoteAgent);
|
await registry.testRegisterAgent(remoteAgent);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('RemoteAgentInvocation', () => {
|
|||||||
displayName: 'Test Agent',
|
displayName: 'Test Agent',
|
||||||
description: 'A test agent',
|
description: 'A test agent',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {},
|
inputSchema: { type: 'object' },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -70,10 +70,33 @@ describe('RemoteAgentInvocation', () => {
|
|||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if query is missing', () => {
|
it('accepts missing query (defaults to "Get Started!")', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new RemoteAgentInvocation(mockDefinition, {}, mockMessageBus);
|
new RemoteAgentInvocation(mockDefinition, {}, mockMessageBus);
|
||||||
}).toThrow("requires a string 'query' input");
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses "Get Started!" default when query is missing during execution', async () => {
|
||||||
|
mockClientManager.getClient.mockReturnValue({});
|
||||||
|
mockClientManager.sendMessage.mockResolvedValue({
|
||||||
|
kind: 'message',
|
||||||
|
messageId: 'msg-1',
|
||||||
|
role: 'agent',
|
||||||
|
parts: [{ kind: 'text', text: 'Hello' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const invocation = new RemoteAgentInvocation(
|
||||||
|
mockDefinition,
|
||||||
|
{},
|
||||||
|
mockMessageBus,
|
||||||
|
);
|
||||||
|
await invocation.execute(new AbortController().signal);
|
||||||
|
|
||||||
|
expect(mockClientManager.sendMessage).toHaveBeenCalledWith(
|
||||||
|
'test-agent',
|
||||||
|
'Get Started!',
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws if query is not a string', () => {
|
it('throws if query is not a string', () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
type ToolResult,
|
type ToolResult,
|
||||||
type ToolCallConfirmationDetails,
|
type ToolCallConfirmationDetails,
|
||||||
} from '../tools/tools.js';
|
} from '../tools/tools.js';
|
||||||
|
import { DEFAULT_QUERY_STRING } from './types.js';
|
||||||
import type {
|
import type {
|
||||||
RemoteAgentInputs,
|
RemoteAgentInputs,
|
||||||
RemoteAgentDefinition,
|
RemoteAgentDefinition,
|
||||||
@@ -89,7 +90,7 @@ export class RemoteAgentInvocation extends BaseToolInvocation<
|
|||||||
_toolName?: string,
|
_toolName?: string,
|
||||||
_toolDisplayName?: string,
|
_toolDisplayName?: string,
|
||||||
) {
|
) {
|
||||||
const query = params['query'];
|
const query = params['query'] ?? DEFAULT_QUERY_STRING;
|
||||||
if (typeof query !== 'string') {
|
if (typeof query !== 'string') {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Remote agent '${definition.name}' requires a string 'query' input.`,
|
`Remote agent '${definition.name}' requires a string 'query' input.`,
|
||||||
|
|||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { convertInputConfigToJsonSchema } from './schema-utils.js';
|
|
||||||
import type { InputConfig } from './types.js';
|
|
||||||
|
|
||||||
const PRIMITIVE_TYPES_CONFIG: InputConfig = {
|
|
||||||
inputs: {
|
|
||||||
goal: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The primary objective',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
max_retries: {
|
|
||||||
type: 'integer',
|
|
||||||
description: 'Maximum number of retries',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
temperature: {
|
|
||||||
type: 'number',
|
|
||||||
description: 'The model temperature',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
verbose: {
|
|
||||||
type: 'boolean',
|
|
||||||
description: 'Enable verbose logging',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ARRAY_TYPES_CONFIG: InputConfig = {
|
|
||||||
inputs: {
|
|
||||||
filenames: {
|
|
||||||
type: 'string[]',
|
|
||||||
description: 'A list of file paths',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
scores: {
|
|
||||||
type: 'number[]',
|
|
||||||
description: 'A list of scores',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const NO_REQUIRED_FIELDS_CONFIG: InputConfig = {
|
|
||||||
inputs: {
|
|
||||||
optional_param: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'An optional parameter',
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ALL_REQUIRED_FIELDS_CONFIG: InputConfig = {
|
|
||||||
inputs: {
|
|
||||||
paramA: { type: 'string', description: 'Parameter A', required: true },
|
|
||||||
paramB: { type: 'boolean', description: 'Parameter B', required: true },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const EMPTY_CONFIG: InputConfig = {
|
|
||||||
inputs: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const UNSUPPORTED_TYPE_CONFIG: InputConfig = {
|
|
||||||
inputs: {
|
|
||||||
invalid_param: {
|
|
||||||
// @ts-expect-error - Intentionally testing an invalid type
|
|
||||||
type: 'date',
|
|
||||||
description: 'This type is not supported',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('convertInputConfigToJsonSchema', () => {
|
|
||||||
describe('type conversion', () => {
|
|
||||||
it('should correctly convert an InputConfig with various primitive types', () => {
|
|
||||||
const result = convertInputConfigToJsonSchema(PRIMITIVE_TYPES_CONFIG);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
goal: { type: 'string', description: 'The primary objective' },
|
|
||||||
max_retries: {
|
|
||||||
type: 'integer',
|
|
||||||
description: 'Maximum number of retries',
|
|
||||||
},
|
|
||||||
temperature: { type: 'number', description: 'The model temperature' },
|
|
||||||
verbose: { type: 'boolean', description: 'Enable verbose logging' },
|
|
||||||
},
|
|
||||||
required: ['goal', 'temperature'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly handle array types for strings and numbers', () => {
|
|
||||||
const result = convertInputConfigToJsonSchema(ARRAY_TYPES_CONFIG);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
filenames: {
|
|
||||||
type: 'array',
|
|
||||||
description: 'A list of file paths',
|
|
||||||
items: { type: 'string' },
|
|
||||||
},
|
|
||||||
scores: {
|
|
||||||
type: 'array',
|
|
||||||
description: 'A list of scores',
|
|
||||||
items: { type: 'number' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ['filenames'],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('required field handling', () => {
|
|
||||||
it('should produce an undefined `required` field when no inputs are required', () => {
|
|
||||||
const result = convertInputConfigToJsonSchema(NO_REQUIRED_FIELDS_CONFIG);
|
|
||||||
|
|
||||||
expect(result.properties['optional_param']).toBeDefined();
|
|
||||||
// Per the implementation and JSON Schema spec, the `required` field
|
|
||||||
// should be omitted if no properties are required.
|
|
||||||
expect(result.required).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should list all properties in `required` when all are marked as required', () => {
|
|
||||||
const result = convertInputConfigToJsonSchema(ALL_REQUIRED_FIELDS_CONFIG);
|
|
||||||
expect(result.required).toHaveLength(2);
|
|
||||||
expect(result.required).toEqual(
|
|
||||||
expect.arrayContaining(['paramA', 'paramB']),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('edge cases', () => {
|
|
||||||
it('should return a valid, empty schema for an empty input config', () => {
|
|
||||||
const result = convertInputConfigToJsonSchema(EMPTY_CONFIG);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
|
||||||
type: 'object',
|
|
||||||
properties: {},
|
|
||||||
required: undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error handling', () => {
|
|
||||||
it('should throw an informative error for an unsupported input type', () => {
|
|
||||||
const action = () =>
|
|
||||||
convertInputConfigToJsonSchema(UNSUPPORTED_TYPE_CONFIG);
|
|
||||||
|
|
||||||
expect(action).toThrow(/Unsupported input type 'date'/);
|
|
||||||
expect(action).toThrow(/parameter 'invalid_param'/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2025 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { InputConfig } from './types.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the structure for a JSON Schema object, used for tool function
|
|
||||||
* declarations.
|
|
||||||
*/
|
|
||||||
interface JsonSchemaObject {
|
|
||||||
type: 'object';
|
|
||||||
properties: Record<string, JsonSchemaProperty>;
|
|
||||||
required?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the structure for a property within a {@link JsonSchemaObject}.
|
|
||||||
*/
|
|
||||||
interface JsonSchemaProperty {
|
|
||||||
type: 'string' | 'number' | 'integer' | 'boolean' | 'array';
|
|
||||||
description: string;
|
|
||||||
items?: { type: 'string' | 'number' };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts an internal `InputConfig` definition into a standard JSON Schema
|
|
||||||
* object suitable for a tool's `FunctionDeclaration`.
|
|
||||||
*
|
|
||||||
* This utility ensures that the configuration for a subagent's inputs is
|
|
||||||
* correctly translated into the format expected by the generative model.
|
|
||||||
*
|
|
||||||
* @param inputConfig The internal `InputConfig` to convert.
|
|
||||||
* @returns A JSON Schema object representing the inputs.
|
|
||||||
* @throws An `Error` if an unsupported input type is encountered, ensuring
|
|
||||||
* configuration errors are caught early.
|
|
||||||
*/
|
|
||||||
export function convertInputConfigToJsonSchema(
|
|
||||||
inputConfig: InputConfig,
|
|
||||||
): JsonSchemaObject {
|
|
||||||
const properties: Record<string, JsonSchemaProperty> = {};
|
|
||||||
const required: string[] = [];
|
|
||||||
|
|
||||||
for (const [name, definition] of Object.entries(inputConfig.inputs)) {
|
|
||||||
const schemaProperty: Partial<JsonSchemaProperty> = {
|
|
||||||
description: definition.description,
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (definition.type) {
|
|
||||||
case 'string':
|
|
||||||
case 'number':
|
|
||||||
case 'integer':
|
|
||||||
case 'boolean':
|
|
||||||
schemaProperty.type = definition.type;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'string[]':
|
|
||||||
schemaProperty.type = 'array';
|
|
||||||
schemaProperty.items = { type: 'string' };
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'number[]':
|
|
||||||
schemaProperty.type = 'array';
|
|
||||||
schemaProperty.items = { type: 'number' };
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
const exhaustiveCheck: never = definition.type;
|
|
||||||
throw new Error(
|
|
||||||
`Unsupported input type '${exhaustiveCheck}' for parameter '${name}'. ` +
|
|
||||||
'Supported types: string, number, integer, boolean, string[], number[]',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
properties[name] = schemaProperty as JsonSchemaProperty;
|
|
||||||
|
|
||||||
if (definition.required) {
|
|
||||||
required.push(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'object',
|
|
||||||
properties,
|
|
||||||
required: required.length > 0 ? required : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
import { SubagentToolWrapper } from './subagent-tool-wrapper.js';
|
import { SubagentToolWrapper } from './subagent-tool-wrapper.js';
|
||||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||||
import { convertInputConfigToJsonSchema } from './schema-utils.js';
|
|
||||||
import { makeFakeConfig } from '../test-utils/config.js';
|
import { makeFakeConfig } from '../test-utils/config.js';
|
||||||
import type { LocalAgentDefinition, AgentInputs } from './types.js';
|
import type { LocalAgentDefinition, AgentInputs } from './types.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
@@ -17,12 +16,8 @@ import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
|
|||||||
|
|
||||||
// Mock dependencies to isolate the SubagentToolWrapper class
|
// Mock dependencies to isolate the SubagentToolWrapper class
|
||||||
vi.mock('./local-invocation.js');
|
vi.mock('./local-invocation.js');
|
||||||
vi.mock('./schema-utils.js');
|
|
||||||
|
|
||||||
const MockedLocalSubagentInvocation = vi.mocked(LocalSubagentInvocation);
|
const MockedLocalSubagentInvocation = vi.mocked(LocalSubagentInvocation);
|
||||||
const mockConvertInputConfigToJsonSchema = vi.mocked(
|
|
||||||
convertInputConfigToJsonSchema,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Define reusable test data
|
// Define reusable test data
|
||||||
let mockConfig: Config;
|
let mockConfig: Config;
|
||||||
@@ -34,14 +29,17 @@ const mockDefinition: LocalAgentDefinition = {
|
|||||||
displayName: 'Test Agent Display Name',
|
displayName: 'Test Agent Display Name',
|
||||||
description: 'An agent for testing.',
|
description: 'An agent for testing.',
|
||||||
inputConfig: {
|
inputConfig: {
|
||||||
inputs: {
|
inputSchema: {
|
||||||
goal: { type: 'string', required: true, description: 'The goal.' },
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
goal: { type: 'string', description: 'The goal.' },
|
||||||
priority: {
|
priority: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
required: false,
|
|
||||||
description: 'The priority.',
|
description: 'The priority.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
required: ['goal'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modelConfig: {
|
modelConfig: {
|
||||||
model: 'gemini-test-model',
|
model: 'gemini-test-model',
|
||||||
@@ -54,34 +52,14 @@ const mockDefinition: LocalAgentDefinition = {
|
|||||||
promptConfig: { systemPrompt: 'You are a test agent.' },
|
promptConfig: { systemPrompt: 'You are a test agent.' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSchema = {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
goal: { type: 'string', description: 'The goal.' },
|
|
||||||
priority: { type: 'number', description: 'The priority.' },
|
|
||||||
},
|
|
||||||
required: ['goal'],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('SubagentToolWrapper', () => {
|
describe('SubagentToolWrapper', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
mockConfig = makeFakeConfig();
|
mockConfig = makeFakeConfig();
|
||||||
mockMessageBus = createMockMessageBus();
|
mockMessageBus = createMockMessageBus();
|
||||||
// Provide a mock implementation for the schema conversion utility
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
mockConvertInputConfigToJsonSchema.mockReturnValue(mockSchema as any);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
it('should call convertInputConfigToJsonSchema with the correct agent inputConfig', () => {
|
|
||||||
new SubagentToolWrapper(mockDefinition, mockConfig, mockMessageBus);
|
|
||||||
|
|
||||||
expect(convertInputConfigToJsonSchema).toHaveBeenCalledExactlyOnceWith(
|
|
||||||
mockDefinition.inputConfig,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly configure the tool properties from the agent definition', () => {
|
it('should correctly configure the tool properties from the agent definition', () => {
|
||||||
const wrapper = new SubagentToolWrapper(
|
const wrapper = new SubagentToolWrapper(
|
||||||
mockDefinition,
|
mockDefinition,
|
||||||
@@ -120,7 +98,9 @@ describe('SubagentToolWrapper', () => {
|
|||||||
|
|
||||||
expect(schema.name).toBe(mockDefinition.name);
|
expect(schema.name).toBe(mockDefinition.name);
|
||||||
expect(schema.description).toBe(mockDefinition.description);
|
expect(schema.description).toBe(mockDefinition.description);
|
||||||
expect(schema.parametersJsonSchema).toEqual(mockSchema);
|
expect(schema.parametersJsonSchema).toEqual(
|
||||||
|
mockDefinition.inputConfig.inputSchema,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from '../tools/tools.js';
|
} from '../tools/tools.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import type { AgentDefinition, AgentInputs } from './types.js';
|
import type { AgentDefinition, AgentInputs } from './types.js';
|
||||||
import { convertInputConfigToJsonSchema } from './schema-utils.js';
|
|
||||||
import { LocalSubagentInvocation } from './local-invocation.js';
|
import { LocalSubagentInvocation } from './local-invocation.js';
|
||||||
import { RemoteAgentInvocation } from './remote-invocation.js';
|
import { RemoteAgentInvocation } from './remote-invocation.js';
|
||||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||||
@@ -40,16 +39,12 @@ export class SubagentToolWrapper extends BaseDeclarativeTool<
|
|||||||
private readonly config: Config,
|
private readonly config: Config,
|
||||||
messageBus: MessageBus,
|
messageBus: MessageBus,
|
||||||
) {
|
) {
|
||||||
const parameterSchema = convertInputConfigToJsonSchema(
|
|
||||||
definition.inputConfig,
|
|
||||||
);
|
|
||||||
|
|
||||||
super(
|
super(
|
||||||
definition.name,
|
definition.name,
|
||||||
definition.displayName ?? definition.name,
|
definition.displayName ?? definition.name,
|
||||||
definition.description,
|
definition.description,
|
||||||
Kind.Think,
|
Kind.Think,
|
||||||
parameterSchema,
|
definition.inputConfig.inputSchema,
|
||||||
messageBus,
|
messageBus,
|
||||||
/* isOutputMarkdown */ true,
|
/* isOutputMarkdown */ true,
|
||||||
/* canUpdateOutput */ true,
|
/* canUpdateOutput */ true,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { Content, FunctionDeclaration } from '@google/genai';
|
|||||||
import type { AnyDeclarativeTool } from '../tools/tools.js';
|
import type { AnyDeclarativeTool } from '../tools/tools.js';
|
||||||
import { type z } from 'zod';
|
import { type z } from 'zod';
|
||||||
import type { ModelConfig } from '../services/modelConfigService.js';
|
import type { ModelConfig } from '../services/modelConfigService.js';
|
||||||
|
import type { AnySchema } from 'ajv';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Describes the possible termination modes for an agent.
|
* Describes the possible termination modes for an agent.
|
||||||
@@ -33,6 +34,11 @@ export interface OutputObject {
|
|||||||
terminate_reason: AgentTerminateMode;
|
terminate_reason: AgentTerminateMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default query string provided to an agent as input.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_QUERY_STRING = 'Get Started!';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the validated input parameters passed to an agent upon invocation.
|
* Represents the validated input parameters passed to an agent upon invocation.
|
||||||
* Used primarily for templating the system prompt. (Replaces ContextState)
|
* Used primarily for templating the system prompt. (Replaces ContextState)
|
||||||
@@ -137,24 +143,7 @@ export interface ToolConfig {
|
|||||||
* Configures the expected inputs (parameters) for the agent.
|
* Configures the expected inputs (parameters) for the agent.
|
||||||
*/
|
*/
|
||||||
export interface InputConfig {
|
export interface InputConfig {
|
||||||
/**
|
inputSchema: AnySchema;
|
||||||
* Defines the parameters the agent accepts.
|
|
||||||
* This is vital for generating the tool wrapper schema.
|
|
||||||
*/
|
|
||||||
inputs: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
description: string;
|
|
||||||
type:
|
|
||||||
| 'string'
|
|
||||||
| 'number'
|
|
||||||
| 'boolean'
|
|
||||||
| 'integer'
|
|
||||||
| 'string[]'
|
|
||||||
| 'number[]';
|
|
||||||
required: boolean;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import AjvPkg from 'ajv';
|
import AjvPkg, { type AnySchema } from 'ajv';
|
||||||
import * as addFormats from 'ajv-formats';
|
import * as addFormats from 'ajv-formats';
|
||||||
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
// Ajv's ESM/CJS interop: use 'any' for compatibility as recommended by Ajv docs
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -47,4 +47,16 @@ export class SchemaValidator {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a JSON schema itself. Returns null if the schema is valid,
|
||||||
|
* otherwise returns a string describing the validation errors.
|
||||||
|
*/
|
||||||
|
static validateSchema(schema: AnySchema | undefined): string | null {
|
||||||
|
if (!schema) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const isValid = ajValidator.validateSchema(schema);
|
||||||
|
return isValid ? null : ajValidator.errorsText(ajValidator.errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user