UNPKG

@dexwox-labs/a2a-core

Version:

Core types, validation and telemetry for Google's Agent-to-Agent (A2A) protocol - shared foundation for client and server implementations

491 lines (459 loc) 14.7 kB
/** * @module Validators * @description Schema validation utilities for A2A protocol types * * This module provides schema definitions and validation functions for the A2A protocol. * It uses Zod for runtime type validation and provides helper functions for validating * various protocol objects. */ import { z } from 'zod'; import { TaskStateSchema, MessagePartSchema, ArtifactSchema, A2AErrorSchema, type TaskTransition, type Message, type MessageSendConfiguration, type Task, type AgentCard, type PushNotificationConfig, type DiscoverRequest, type DiscoverResponse, } from '../types/a2a-protocol'; // Extended schemas with additional validation const TaskTransitionSchema: z.ZodType<TaskTransition> = z.object({ from: TaskStateSchema, to: TaskStateSchema, timestamp: z.string().datetime(), reason: z.string().optional(), }); // Define the message part schemas individually const TextMessagePartSchema = z.object({ type: z.literal('text'), content: z.string(), format: z.enum(['plain', 'markdown'] as const).default('plain'), }); const FileMessagePartSchema = z.object({ type: z.literal('file'), content: z.union([z.string(), z.instanceof(Uint8Array)]), mimeType: z.string(), name: z.string(), size: z.number().optional(), }); const DataMessagePartSchema = z.object({ type: z.literal('data'), content: z.record(z.any()), schema: z.string().optional(), }); const HeartbeatMessagePartSchema = z.object({ type: z.literal('heartbeat'), content: z.string(), format: z.literal('plain').default('plain'), }); // Create the union type const StrictMessagePartSchema = z.union([ TextMessagePartSchema, FileMessagePartSchema, DataMessagePartSchema, HeartbeatMessagePartSchema, ]); const MessageSchema = z.object({ parts: z.array(StrictMessagePartSchema).min(1, 'At least one message part is required'), taskId: z.string().uuid().optional(), contextId: z.string().uuid().optional(), }) as z.ZodType<Message>; const MessageSendConfigurationSchema: z.ZodType<MessageSendConfiguration> = z.object({ priority: z.number().int().min(0).max(100).optional(), timeout: z.number().int().positive().optional(), metadata: z.record(z.unknown()).optional(), }); const TaskSchema = z.object({ id: z.string().uuid(), name: z.string().min(1, 'Task name is required'), description: z.string().optional(), status: TaskStateSchema, agentId: z.string().optional(), parts: z.array(StrictMessagePartSchema).optional(), expectedParts: z.number().int().positive().optional(), artifacts: z.array(ArtifactSchema).optional(), transitions: z.array(TaskTransitionSchema).optional(), createdAt: z.string().datetime(), updatedAt: z.string().datetime(), error: A2AErrorSchema.optional(), metadata: z.record(z.unknown()).optional(), contextId: z.string().uuid().optional(), inputSchema: z.record(z.any()).optional(), outputSchema: z.record(z.any()).optional(), input: z.record(z.any()).optional(), output: z.record(z.any()).optional(), }) as z.ZodType<Task>; const AgentCardSchema: z.ZodType<AgentCard> = z.object({ id: z.string().uuid(), name: z.string().min(1, 'Agent name is required'), capabilities: z.array(z.string().min(1, 'Capability cannot be empty')), endpoint: z.string().url('Endpoint must be a valid URL'), metadata: z.record(z.unknown()).optional(), }); const PushNotificationConfigSchema: z.ZodType<PushNotificationConfig> = z.object({ enabled: z.boolean(), endpoint: z.string().url('Endpoint must be a valid URL').optional(), authToken: z.string().optional(), events: z.array(z.string().min(1, 'Event name cannot be empty')), metadata: z.record(z.unknown()).optional(), }); const DiscoverRequestSchema: z.ZodType<DiscoverRequest> = z.object({ id: z.string().min(1, 'ID is required'), method: z.literal('discover'), params: z.record(z.unknown()).optional(), jsonrpc: z.literal('2.0').optional(), }); const DiscoverResponseSchema: z.ZodType<DiscoverResponse> = z.object({ id: z.string(), result: z.array(AgentCardSchema), error: A2AErrorSchema.optional(), }); /** * Validation functions for A2A protocol objects * * These functions validate objects against their respective schemas and return * a SafeParseReturnType that includes either the validated data or validation errors. */ /** * Validates a message object against the Message schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated Message or validation errors * * @example * ```typescript * const result = validateMessage({ * parts: [{ type: 'text', content: 'Hello, world!' }] * }); * * if (result.success) { * // Use the validated message * console.log('Valid message:', result.data); * } else { * // Handle validation errors * console.error('Invalid message:', result.error); * } * ``` */ export const validateMessage = (data: unknown): z.SafeParseReturnType<unknown, Message> => MessageSchema.safeParse(data); /** * Validates a task object against the Task schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated Task or validation errors * * @example * ```typescript * const result = validateTask({ * id: '123e4567-e89b-12d3-a456-426614174000', * name: 'Process Data', * status: 'submitted', * createdAt: new Date().toISOString(), * updatedAt: new Date().toISOString() * }); * * if (result.success) { * // Use the validated task * console.log('Valid task:', result.data); * } else { * // Handle validation errors * console.error('Invalid task:', result.error); * } * ``` */ export const validateTask = (data: unknown): z.SafeParseReturnType<unknown, Task> => TaskSchema.safeParse(data); /** * Validates an agent card object against the AgentCard schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated AgentCard or validation errors * * @example * ```typescript * const result = validateAgentCard({ * id: '123e4567-e89b-12d3-a456-426614174000', * name: 'Weather Agent', * capabilities: ['weather-forecasting', 'location-search'], * endpoint: 'https://example.com/agents/weather' * }); * * if (result.success) { * // Use the validated agent card * console.log('Valid agent card:', result.data); * } else { * // Handle validation errors * console.error('Invalid agent card:', result.error); * } * ``` */ export const validateAgentCard = (data: unknown): z.SafeParseReturnType<unknown, AgentCard> => AgentCardSchema.safeParse(data); /** * Validates a push notification configuration against the PushNotificationConfig schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated PushNotificationConfig or validation errors * * @example * ```typescript * const result = validatePushNotificationConfig({ * enabled: true, * endpoint: 'https://example.com/webhooks/a2a', * authToken: 'secret-token', * events: ['taskCompleted', 'taskFailed'] * }); * * if (result.success) { * // Use the validated config * console.log('Valid push config:', result.data); * } else { * // Handle validation errors * console.error('Invalid push config:', result.error); * } * ``` */ export const validatePushNotificationConfig = (data: unknown): z.SafeParseReturnType<unknown, PushNotificationConfig> => PushNotificationConfigSchema.safeParse(data); /** * Validates a discover request against the DiscoverRequest schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated DiscoverRequest or validation errors * * @example * ```typescript * const result = validateDiscoverRequest({ * id: '1', * method: 'discover', * params: { capability: 'weather-forecasting' }, * jsonrpc: '2.0' * }); * * if (result.success) { * // Use the validated request * console.log('Valid discover request:', result.data); * } else { * // Handle validation errors * console.error('Invalid discover request:', result.error); * } * ``` */ export const validateDiscoverRequest = (data: unknown): z.SafeParseReturnType<unknown, DiscoverRequest> => DiscoverRequestSchema.safeParse(data); /** * Validates a discover response against the DiscoverResponse schema * * @param data - The data to validate * @returns A SafeParseReturnType containing either the validated DiscoverResponse or validation errors * * @example * ```typescript * const result = validateDiscoverResponse({ * id: '1', * result: [ * { * id: '123e4567-e89b-12d3-a456-426614174000', * name: 'Weather Agent', * capabilities: ['weather-forecasting'], * endpoint: 'https://example.com/agents/weather' * } * ] * }); * * if (result.success) { * // Use the validated response * console.log('Valid discover response:', result.data); * } else { * // Handle validation errors * console.error('Invalid discover response:', result.error); * } * ``` */ export const validateDiscoverResponse = (data: unknown): z.SafeParseReturnType<unknown, DiscoverResponse> => DiscoverResponseSchema.safeParse(data); /** * Type guards for A2A protocol objects * * These functions check if an object conforms to a specific schema and narrow * its type if it does. They return a boolean indicating whether the object is * of the specified type. */ /** * Checks if the provided data is a valid Message * * @param data - The data to check * @returns True if the data is a valid Message, false otherwise * * @example * ```typescript * const data = { parts: [{ type: 'text', content: 'Hello' }] }; * * if (isMessage(data)) { * // TypeScript now knows that data is a Message * console.log('Message parts:', data.parts.length); * } * ``` */ export const isMessage = (data: unknown): data is Message => MessageSchema.safeParse(data).success; /** * Checks if the provided data is a valid Task * * @param data - The data to check * @returns True if the data is a valid Task, false otherwise * * @example * ```typescript * const data = getTaskFromSomewhere(); * * if (isTask(data)) { * // TypeScript now knows that data is a Task * console.log('Task status:', data.status); * } * ``` */ export const isTask = (data: unknown): data is Task => TaskSchema.safeParse(data).success; /** * Checks if the provided data is a valid AgentCard * * @param data - The data to check * @returns True if the data is a valid AgentCard, false otherwise * * @example * ```typescript * const data = getAgentFromSomewhere(); * * if (isAgentCard(data)) { * // TypeScript now knows that data is an AgentCard * console.log('Agent capabilities:', data.capabilities); * } * ``` */ export const isAgentCard = (data: unknown): data is AgentCard => AgentCardSchema.safeParse(data).success; /** * Checks if the provided data is a valid PushNotificationConfig * * @param data - The data to check * @returns True if the data is a valid PushNotificationConfig, false otherwise * * @example * ```typescript * const data = getConfigFromSomewhere(); * * if (isPushNotificationConfig(data)) { * // TypeScript now knows that data is a PushNotificationConfig * console.log('Push notifications enabled:', data.enabled); * } * ``` */ export const isPushNotificationConfig = (data: unknown): data is PushNotificationConfig => PushNotificationConfigSchema.safeParse(data).success; /** * Formats a Zod validation error into a human-readable string * * This function takes a Zod error object and converts it into a string * representation, with each issue formatted as "path: message" and joined * with semicolons. * * @param error - The Zod error to format * @returns A formatted error string * * @example * ```typescript * const result = validateTask(invalidTask); * * if (!result.success) { * const errorMessage = formatValidationError(result.error); * console.error('Validation failed:', errorMessage); * // Example output: "name: Required; status: Invalid enum value" * } * ``` */ export function formatValidationError(error: z.ZodError): string { return error.issues .map(issue => { const path = issue.path.join('.'); return path ? `${path}: ${issue.message}` : issue.message; }) .join('; '); } /** * Decorator for validating method parameters against a schema * * This decorator can be applied to methods to automatically validate their * first parameter against the provided schema. If validation fails, an error * is thrown with details about the validation issues. * * @param schema - The Zod schema to validate against * @returns A method decorator function * * @example * ```typescript * class MessageService { * @validate(MessageSchema) * sendMessage(message: Message) { * // This code only runs if message passes validation * console.log('Sending message:', message); * } * } * * const service = new MessageService(); * * // This will throw an error because the message is invalid * service.sendMessage({ invalid: 'data' }); * ``` */ export function validate<T extends z.ZodTypeAny>(schema: T) { return function ( target: any, propertyKey: string, descriptor: PropertyDescriptor ) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { const result = schema.safeParse(args[0]); if (!result.success) { throw new Error(`Validation failed: ${formatValidationError(result.error)}`); } return originalMethod.apply(this, args); }; return descriptor; }; } /** * Collection of all schema definitions for A2A protocol objects * * This object provides access to all the Zod schemas defined in this module, * allowing them to be used directly for validation or type inference. * * @example * ```typescript * // Use a schema directly for validation * const result = schemas.Message.safeParse(data); * * // Create a type from a schema * type MessageType = z.infer<typeof schemas.Message>; * ``` */ export const schemas = { Message: MessageSchema, Task: TaskSchema, AgentCard: AgentCardSchema, PushNotificationConfig: PushNotificationConfigSchema, DiscoverRequest: DiscoverRequestSchema, DiscoverResponse: DiscoverResponseSchema, MessagePart: MessagePartSchema, Artifact: ArtifactSchema, A2AError: A2AErrorSchema, TaskTransition: TaskTransitionSchema, MessageSendConfiguration: MessageSendConfigurationSchema, };