UNPKG

@artinet/sdk

Version:

A TypeScript SDK for building collaborative AI agents.

353 lines (352 loc) 11.6 kB
/** * Copyright 2025 The Artinet Project * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Agent Factory * * This module provides a fluent builder API for constructing A2A agents and * execution engines. It enables declarative definition of multi-step agent * workflows with type-safe step composition and automatic execution orchestration. * * **Key Features:** * - Fluent API with method chaining (`.text()`, `.data()`, `.file()`, etc.) * - Type-safe argument passing between steps via `args` carry pattern * - Multiple output types: text, file, data, message, artifact, status, task * - Agent-to-agent orchestration via `.sendMessage()` * - Static value shortcuts for simple steps * - Step skipping via `skip()` function * * @module AgentFactory * @version 0.6 * @since 0.5.6 * @author The Artinet Project */ import { A2A } from "../types/index.js"; import * as transform from './transform.js'; import { createAgent as createAgentImpl } from '../services/a2a/factory/service.js'; import { describe } from './index.js'; import { logger } from "../config/index.js"; import { v4 as uuidv4 } from 'uuid'; import { createStepEngine, } from './create.js'; const toFunction = (function_or_ret) => { return typeof function_or_ret === 'function' ? function_or_ret : () => function_or_ret; }; /** * Fluent builder for constructing A2A agent execution engines. * * AgentFactory provides a type-safe, fluent API for composing multi-step * agent workflows. It supports method chaining to build complex agent behaviors * from individual processing steps, with automatic type inference for carried * arguments between steps. * * @template I - The arguments type received from previous steps (inferred automatically) * * @example * ```typescript * // Basic agent with text steps * const agent = cr8("MyAgent") * .text(({ content }) => `You said: ${content}`) * .agent; * * // Agent with carried args between steps * const agent = cr8("AnalysisAgent") * .text(({ content }) => ({ * reply: `Analyzing: ${content}`, * args: { originalContent: content } * })) * .data(({ args }) => ({ * wordCount: args?.originalContent?.split(' ').length, * timestamp: Date.now() * })) * .text(({ args }) => `Analysis complete: ${args?.wordCount} words`) * .agent; * * // Agent-to-agent orchestration * const orchestrator = cr8("Orchestrator") * .text("Starting multi-agent workflow...") * .sendMessage({ agent: otherAgent, message: "Process this" }) * .text(({ args }) => `Got result: ${args?.task?.status.state}`) * .agent; * ``` * * @public * @since 0.5.6 */ export class AgentFactory { _agentCard; _params; _steps; _serve; /** * Protected constructor to enforce factory method usage. * @param agentCard - The agent card to use * @param params - The parameters to use * @param steps - Initial steps array */ constructor(_agentCard, _params, //@typescript-eslint/no-explicit-any _steps = [], _serve = () => { throw new Error('Serve function not provided'); }) { this._agentCard = _agentCard; this._params = _params; this._steps = _steps; this._serve = _serve; } /** * Builds the step list for the workflow. * * @returns Array of workflow steps * @throws Error if no steps have been added * * @example * ```typescript * const steps = cr8.steps; * ``` */ //@typescript-eslint/no-explicit-any get steps() { return this._steps; } /** * The {@link A2A.AgentCard} to use * @returns The {@link A2A.AgentCard} */ get agentCard() { return this._agentCard; } /** * The {@link FactoryParams} to use * @returns The {@link FactoryParams} */ get params() { return this._params; } /** * Creates an agent execution engine from the built workflow. * * @returns The {@link A2A.Engine} * * @example * ```typescript * const engine = builder.engine; * // Use engine with service execution * ``` */ get engine() { return createStepEngine(this.steps); } /** * Creates a complete A2A agent using the built workflow. * * @param params - The {@link ServiceParams} to use * @returns The {@link Service} * * @example * ```typescript * const agent = cr8({ * id: 'my-agent', * name: 'Assistant Agent', * capabilities: ['text-processing'] * }).agent; * ``` */ get agent() { return createAgentImpl({ ...this._params, agentCard: this._agentCard, engine: this.engine, }); } get server() { return this._serve({ agent: this.agent, serverParams: this._params, }); } from(engine = this.engine) { return createAgentImpl({ ...this._params, agentCard: this._agentCard, engine: engine, }); } serve(engine = this.engine) { return this._serve({ agent: this.from(engine), serverParams: this._params, }); } addStep(step) { return new AgentFactory(this._agentCard, this._params, [...this.steps, step], this._serve); } text(step_or_text) { const stepFn = toFunction(step_or_text); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['text'], handler: transform.Parts('text'), }); } file(step_or_file) { const stepFn = toFunction(step_or_file); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['file'], handler: transform.Parts('file'), }); } data(step_or_data) { const stepFn = toFunction(step_or_data); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['data'], handler: transform.Parts('data'), }); } message(step_or_message) { const stepFn = toFunction(step_or_message); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['message'], handler: transform.Message(), }); } artifact(step_or_artifact) { const stepFn = toFunction(step_or_artifact); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['artifact-update'], handler: transform.Artifact(), }); } status(step_or_status) { const stepFn = toFunction(step_or_status); return this.addStep({ id: uuidv4(), step: stepFn, kind: A2A.Kind['status-update'], handler: transform.Status(), }); } task(step_or_task_or_string) { const stepFn = toFunction(step_or_task_or_string); return this.addStep({ id: uuidv4(), step: stepFn, kind: 'task', handler: transform.Task(), }); } /** * Adds an agent-to-agent orchestration step to the workflow. * * This step sends a message to another agent (local Service or remote A2A Server) * and yields the response as a task. Enables multi-agent workflows where one * agent delegates work to others. * * **Note:** This is currently a blocking call. Streaming responses are not * yet supported in orchestration steps. * @note Args passed from the previous step are inserted, by default, * (`unshift`) as `DataPart`s onto the forwarded `Message`.`Parts`. * * @param agent - The target agent (Agent or AgentMessenger) * @param message - Message to send (defaults to context.userMessage) * @returns New builder instance with task carry args (args.task) * * @example * ```typescript * // Delegate to another agent * const orchestrator = cr8("Orchestrator") * .text("Starting workflow...") * .sendMessage({ agent: analysisAgent, message: "Analyze this data" }) * .text(({ args }) => `Analysis result: ${args?.task?.status.state}`) * .agent; * * // Chain multiple agents * const pipeline = cr8("Pipeline") * .sendMessage({ agent: preprocessor }) * .sendMessage({ agent: analyzer }) * .sendMessage({ agent: postprocessor }) * .text(({ args }) => `Final result: ${args?.task?.status.message}`) * .agent; * * // Forward user's message to another agent * const proxy = cr8("Proxy") * .sendMessage({ agent: targetAgent }) // uses context.userMessage * .agent; * ``` */ sendMessage(agent_and_message) { const stepFn = async ({ context, args }) => { logger.info('sendMessage: Sending message: ', { agent: agent_and_message.agent.constructor.name, }); const messageSendParams = describe.messageSendParams(agent_and_message.message ?? structuredClone(context.userMessage)); if (args) { /**We extract the parts of the first A2A protocol object we encounter in the args */ if (args.task || args.message || args.update) { const parts = []; if (args.message) { parts.push(...(A2A.MessageSchema.safeParse(args.message).data?.parts ?? [])); } if (args.task) { parts.push(...(A2A.TaskSchema.safeParse(args.task).data?.status?.message?.parts ?? [])); } if (args.update) { parts.push(...(A2A.TaskStatusUpdateEventSchema.safeParse(args.update).data?.status?.message?.parts ?? [])); parts.push(...(A2A.TaskArtifactUpdateEventSchema.safeParse(args.update).data?.artifact?.parts ?? [])); } parts.forEach((part) => { messageSendParams.message.parts.unshift(part); }); } else { messageSendParams.message.parts.unshift(describe.part.data({ ...args })); } } const response = await agent_and_message.agent .sendMessage(messageSendParams) .catch((error) => { logger.error('sendMessage: Error sending message: ', error); return null; }); if (!response) { logger.warn('sendMessage: No response from agent'); } const task = response ? describe.task({ ...response, taskId: context.taskId, contextId: context.contextId, }) : describe.task({ taskId: context.taskId, contextId: context.contextId, state: A2A.TaskState.working, message: describe.message('No response from agent'), }); return { reply: task, args: { task, }, }; }; return this.addStep({ id: uuidv4(), step: stepFn, kind: 'task', handler: transform.Task(), }); } }