UNPKG

@artinet/sdk

Version:

A TypeScript SDK for building collaborative AI agents.

489 lines (488 loc) 15.6 kB
/** * Copyright 2025 The Artinet Project * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview A2A Agent Builder and Execution Engine 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 * * **Basic Usage:** * ```typescript * import { cr8 } from "@artinet/sdk"; * * const agent = cr8("MyAgent") * .text(({ content }) => `You said: ${content}`) * .data(({ content }) => ({ length: content?.length })) * .agent; * ``` * * @module A2ABuilder * @version 0.6.0-preview * @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 { extractTextContent } from "../services/a2a/helpers/content.js"; import { logger } from "../config/index.js"; import { v4 as uuidv4 } from "uuid"; import { formatJson } from "../utils/index.js"; import { serve } from "../server/express/server.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; /** * 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 = []) { this._agentCard = _agentCard; this._params = _params; this._steps = _steps; } /** * 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 serve({ agent: this.agent, ...this._params, }); } from(engine = this.engine) { return createAgentImpl({ ...this._params, agentCard: this._agentCard, engine: engine, }); } serve(engine = this.engine) { return serve({ agent: this.from(engine), ...this._params, }); } addStep(step) { return new AgentFactory(this._agentCard, this._params, [ ...this.steps, step, ]); } 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 ?? context.userMessage); if (args) { 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, state: A2A.TaskState.working, 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(), }); } /** * Creates a new AgentFactory instance. * * @template Input - The initial arguments type * @returns A new AgentFactory instance * * @example * ```typescript * const factory = AgentFactory.create(myCard, { params }); * ``` */ static create(agentCard, params) { return new AgentFactory(describe.card(agentCard), params); } } /** * Creates a new AgentFactory instance for building agent workflows. * * This is the primary entry point for the fluent builder API. Accepts an * agent card (or name string) and optional factory parameters. * * @param agentCard - Agent card object or name string * @param params - Optional factory parameters (basePath, port, etc.) * @returns New AgentFactory instance * * @example * ```typescript * // Simple agent with name string * const agent = cr8("MyAgent") * .text(({ content }) => `Echo: ${content}`) * .agent; * * // Agent with full card and params * const agent = cr8(myAgentCard, { basePath: "/api" }) * .text("Hello!") * .data(({ content }) => analyzeContent(content)) * .agent; * * // Get the engine directly * const engine = cr8("Processor") * .text("Processing...") * .engine; * * // Create and start server * const server = cr8("ServerAgent", { port: 3000 }) * .text("Ready to serve!") * .server.start(); * ``` * * @public * @since 0.6.0 */ export const cr8 = AgentFactory.create; /** * @deprecated Use {@link cr8} instead * @note This export exists only to alert users that `AgentBuilder` is deprecated. * `AgentBuilder` no longer comes with the `createAgent` method. * @since 0.6.0 */ export class AgentBuilder extends AgentFactory { constructor(agentCard = "default", params) { super(describe.card(typeof agentCard === "string" ? { name: agentCard } : agentCard), params); } } /** * Creates an agent execution engine from a list of workflow steps. * * This function transforms a list of resolved step definitions into an executable * A2A engine that processes contexts through the defined workflow. The engine * is an async generator that yields updates as each step completes. * * **Execution Flow:** * 1. Yields "submitted" status update * 2. Executes each step in order, yielding transformed results * 3. Passes carried args from one step to the next * 4. Yields final task on completion * * @param stepsList - Array of resolved workflow steps (from AgentFactory.steps) * @returns A2A.Engine async generator function * @throws Error if stepsList is empty * * @example * ```typescript * // Typically accessed via AgentFactory * const engine = cr8("MyAgent") * .text("Hello") * .data({ timestamp: Date.now() }) * .engine; * * // Or create manually from steps * const engine = createStepEngine(factory.steps); * * // Execute the engine * for await (const update of engine(context)) { * console.log(update.kind, update); * } * ``` * * @public * @since 0.5.6 */ export function createStepEngine(stepsList) { if (stepsList.length === 0) { throw new Error("No steps provided"); } return async function* (context) { logger.info(`engine[context:${context.contextId}]: starting`); logger.debug(`engine[context:${context.contextId}]: taskId: ${context.taskId}`); const content = extractTextContent(context.userMessage); let _skipStep = false; const input = { message: context.messages, context: context, content: content, skip: () => { _skipStep = true; return; }, }; const submitted = describe.update.submitted({ contextId: context.contextId, taskId: context.taskId, }); logger.debug(`engine[context:${context.contextId}]: submitted`); yield submitted; for (const step of stepsList) { if (await context.isCancelled()) { break; } logger.debug(`engine[context:${context.contextId}]: executing step[${step.id}]: ${step.kind}`); const ret = await step.step({ ...input }); if (_skipStep) { _skipStep = false; logger.debug(`engine[context:${context.contextId}]: skipping step[${step.id}]`); continue; } logger.debug(`engine[context:${context.contextId}]: transforming step[${step.id}]`); const carried = yield* transform.Reply(ret, context, step.handler); input.args = carried; } const task = await context.getTask(); logger.debug(`engine[context:${context.contextId}]: completed task[${task.id}]: ${formatJson(task)}`); yield task; }; }