UNPKG

@artinet/sdk

Version:

A TypeScript SDK for building collaborative AI agents.

762 lines (761 loc) 26.3 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 A from "./agent-builder.js"; import { ServiceParams } from "../services/a2a/factory/service.js"; import { describe } from "./index.js"; import { Service } from "../services/a2a/service.js"; import { MessageParams } from "./message-builder.js"; import { StatusUpdateParams, ArtifactUpdateParams, TaskParams } from "./task-builder.js"; import { ServerParams } from "../server/express/server.js"; export interface MessageSender { sendMessage(params: A2A.MessageSendParams): Promise<A2A.SendMessageSuccessResult>; } /** * Type alias for text-based workflow steps. * * This type represents a step that processes or generates text content * within an agent workflow. Text steps are the most common type of step * and are used for message processing, content generation, and text-based * decision making. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Simple text return * const greetingStep: textStep = async ({ content }) => { * return `Hello! You said: ${content}`; * }; * * // With carry args for next step * const analyzeStep: textStep = async ({ content }) => { * return { * reply: `Analyzed: ${content}`, * args: { sentiment: 'positive', length: content?.length ?? 0 } * }; * }; * ``` * * @public * @since 0.5.6 */ export type textStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A2A.TextPart["text"], Input, Carry>; /** * Type alias for file-based workflow steps. * * This type represents a step that processes or generates file content * within an agent workflow. File steps handle document processing, * file generation, and file-based data operations. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Return file with URI * const downloadStep: fileStep = async ({ content }) => { * return { uri: `https://example.com/files/${content}.pdf` }; * }; * * // Return file with bytes * const generateStep: fileStep = async ({ args }) => { * return { * name: 'report.pdf', * mimeType: 'application/pdf', * bytes: generatePDF(args?.data) * }; * }; * * // Return multiple files * const batchStep: fileStep = async () => { * return [ * { uri: 'https://example.com/file1.pdf' }, * { uri: 'https://example.com/file2.pdf' } * ]; * }; * ``` * * @public * @since 0.5.6 */ export type fileStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A2A.FilePart["file"], Input, Carry>; /** * Type alias for data-based workflow steps. * * This type represents a step that processes or generates structured data * within an agent workflow. Data steps handle JSON processing, API responses, * and structured data transformations. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Return structured data * const analyzeStep: dataStep = async ({ content }) => { * const analysis = await analyzeMessage(content); * return { * sentiment: analysis.sentiment, * entities: analysis.entities, * confidence: analysis.confidence * }; * }; * * // With carry args * const processStep: dataStep = async ({ args }) => { * return { * reply: { processed: true, input: args?.rawData }, * args: { processedAt: Date.now() } * }; * }; * ``` * * @public * @since 0.5.6 */ export type dataStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A2A.DataPart["data"], Input, Carry>; /** * Type alias for message-based workflow steps. * * This type represents a step that constructs or transforms complete A2A messages. * Message steps are useful when you need full control over the message structure, * including role, parts, and metadata. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Return a string (auto-converted to message) * const simpleStep: messageStep = async () => { * return "Hello from the agent!"; * }; * * // Return a full message object * const fullStep: messageStep = async ({ context }) => { * return { * role: "agent", * parts: [ * { kind: "text", text: "Here is your report:" }, * { kind: "file", file: { uri: "https://example.com/report.pdf" } } * ] * }; * }; * ``` * * @public * @since 0.6.0 */ export type messageStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A.Stateless<MessageParams>, Input, Carry>; /** * Type alias for artifact-based workflow steps. * * This type represents a step that creates or updates artifacts within an agent * workflow. Artifacts are persistent, versioned outputs that can be referenced * across task sessions - useful for documents, generated files, or any content * that should be retrievable later. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Create an artifact * const createArtifact: artifactStep = async ({ context, args }) => { * return { * artifactId: `report-${context.taskId}`, * name: "Analysis Report", * parts: [{ kind: "text", text: args?.analysisResult }] * }; * }; * * // Using describe helper * const helperStep: artifactStep = async ({ context }) => { * return describe.artifact({ * artifactId: context.taskId, * parts: [{ kind: "text", text: "Generated content" }] * }); * }; * ``` * * @public * @since 0.6.0 */ export type artifactStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A.Stateless<ArtifactUpdateParams>, Input, Carry>; /** * Type alias for status update workflow steps. * * This type represents a step that emits task status updates within an agent * workflow. Status steps are useful for communicating progress, state changes, * or intermediate results to the client during long-running operations. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Simple status string * const progressStep: statusStep = async () => { * return "working"; * }; * * // Status with message * const detailedStep: statusStep = async ({ args }) => { * return { * status: { * state: A2A.TaskState.working, * message: describe.message(`Processing step ${args?.step} of 5...`) * } * }; * }; * * // Mark completion * const completeStep: statusStep = async () => { * return { status: { state: A2A.TaskState.completed } }; * }; * ``` * * @public * @since 0.6.0 */ export type statusStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A.Stateless<StatusUpdateParams>, Input, Carry>; /** * Type alias for task-based workflow steps. * * This type represents a step that creates or manipulates complete A2A tasks. * Task steps provide full control over the task object, including status, * artifacts, and history. Useful for complex orchestration scenarios or * when you need to return a complete task representation. * * @template Input - Arguments received from previous step * @template Carry - Arguments passed to next step * * @example * ```typescript * // Return task from string * const simpleTask: taskStep = async () => { * return "Task completed successfully"; * }; * * // Return full task object * const fullTask: taskStep = async ({ context, args }) => { * return describe.task({ * id: context.taskId, * contextId: context.contextId, * status: { state: A2A.TaskState.completed }, * artifacts: [args?.generatedArtifact] * }); * }; * * // With carry for chaining * const chainTask: taskStep = async ({ context }) => { * const task = describe.task({ id: context.taskId }); * return { reply: task, args: { taskSnapshot: task } }; * }; * ``` * * @public * @since 0.6.0 */ export type taskStep<Input extends A.bargs = A.empty, Carry extends A.bargs = A.empty> = A.Step<A.Stateless<TaskParams>, Input, Carry>; export type FactoryParams = Omit<ServiceParams, "engine" | "agentCard"> & Omit<ServerParams, "agent">; /** * 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 declare class AgentFactory<I extends A.bargs = A.empty> implements A.AgentBuilder<I> { private readonly _agentCard; private readonly _params?; private readonly _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 */ protected constructor(_agentCard: A2A.AgentCard, _params?: FactoryParams | undefined, _steps?: Array<A.Resolved<any, any, any, any, any>>); /** * 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; * ``` */ get steps(): Array<A.Resolved<any, any, any, any, any>>; /** * The {@link A2A.AgentCard} to use * @returns The {@link A2A.AgentCard} */ get agentCard(): A2A.AgentCard; /** * The {@link FactoryParams} to use * @returns The {@link FactoryParams} */ get params(): FactoryParams | undefined; /** * 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(): A2A.Engine; /** * 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(): Service; get server(): { app: import("express").Express; agent: Service; start: (_port?: number) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>; }; from(engine?: A2A.Engine): Service; serve(engine?: A2A.Engine): { app: import("express").Express; agent: Service; start: (_port?: number) => import("node:http").Server<typeof import("node:http").IncomingMessage, typeof import("node:http").ServerResponse>; }; addStep<Ret extends A.AcceptedReturnValues = A.text, C extends A.bargs = A.empty, R extends A.rep<Ret, C> = A.rep<Ret, C>, Kind extends A.AcceptedKinds = "text">(step: A.Resolved<Ret, I, C, R, Kind>): AgentFactory<A.inferCarry<R>>; /** * Adds a text processing step to the workflow. * * Text steps are the most common step type, producing text content that * becomes a TextPart in the agent's response message. * * @param step - A text step function or static string value * @returns New builder instance with updated type parameters * * @example * ```typescript * // Static text * builder.text("Hello, world!") * * // Dynamic text from content * builder.text(({ content }) => `You said: ${content}`) * * // With carried args * builder.text(({ args }) => ({ * reply: `Processing ${args?.itemCount} items`, * args: { processedAt: Date.now() } * })) * ``` */ text<C extends A.bargs = A.empty>(text: A.text): AgentFactory<A.inC<A.rep<A.text, C>>>; text<C extends A.bargs = A.empty>(step: textStep<I, C>): AgentFactory<A.inC<A.rep<A.text, C>>>; /** * Adds a file processing step to the workflow. * * File steps produce file content that becomes a FilePart in the agent's * response. Files can be specified by URI or inline bytes/base64 content. * * @param step - A file step function or static file object * @returns New builder instance with updated type parameters * * @example * ```typescript * // Static file by URI * builder.file({ uri: "https://example.com/doc.pdf" }) * * // Dynamic file generation * builder.file(async ({ args }) => ({ * name: 'report.pdf', * mimeType: 'application/pdf', * bytes: await generatePDF(args?.data) * })) * * // Multiple files * builder.file(() => [ * { uri: "https://example.com/file1.pdf" }, * { uri: "https://example.com/file2.pdf" } * ]) * ``` */ file<C extends A.bargs = A.empty, R extends A.rep<A.file, C> = A.rep<A.file, C>>(file: A.file): AgentFactory<A.inC<A.rep<A.file, C>>>; file<C extends A.bargs = A.empty, R extends A.rep<A.file, C> = A.rep<A.file, C>>(step: fileStep<I, C>): AgentFactory<A.inC<A.rep<A.file, C>>>; /** * Adds a data processing step to the workflow. * * Data steps produce structured JSON data that becomes a DataPart in the * agent's response. Useful for returning complex objects, API responses, * or any structured data. * * @param step - A data step function or static data object * @returns New builder instance with updated type parameters * * @example * ```typescript * // Static data * builder.data({ status: "ok", version: "1.0.0" }) * * // Dynamic data * builder.data(async ({ content }) => ({ * analysis: await analyzeText(content), * timestamp: Date.now() * })) * * // With carried args * builder.data(({ args }) => ({ * reply: { result: args?.computedValue * 2 }, * args: { doubled: true } * })) * ``` */ data<C extends A.bargs = A.empty>(data: A.data): AgentFactory<A.inC<A.rep<A.data, C>>>; data<C extends A.bargs = A.empty>(step: dataStep<I, C>): AgentFactory<A.inC<A.rep<A.data, C>>>; /** * Adds a message step to the workflow. * * Message steps yield complete A2A messages with full control over role, * parts, and metadata. Use when you need to construct complex multi-part * messages or control the message structure directly. * * @param step - A message step function or static message/string * @returns New builder instance with updated type parameters * * @example * ```typescript * // Simple string message * builder.message("Hello from the agent!") * * // Full message with parts * builder.message(({ context }) => ({ * role: "agent", * parts: [ * { kind: "text", text: "Here are your files:" }, * { kind: "file", file: { uri: "https://example.com/doc.pdf" } } * ] * })) * * // Using describe helper * builder.message(({ args }) => describe.message({ * role: "agent", * parts: [{ kind: "text", text: args?.greeting }] * })) * ``` */ message<C extends A.bargs = A.empty>(message: A.sMessage): AgentFactory<A.inC<A.rep<A.sMessage, C>>>; message<C extends A.bargs = A.empty>(step: messageStep<I, C>): AgentFactory<A.inC<A.rep<A.sMessage, C>>>; /** * Adds an artifact step to the workflow. * * Artifact steps create persistent, versioned outputs that can be referenced * across task sessions. Use for documents, generated files, or content that * clients may need to retrieve later. * * @param step - An artifact step function or static artifact object * @returns New builder instance with updated type parameters * * @example * ```typescript * // Static artifact * builder.artifact(describe.artifact({ * artifactId: "report-001", * parts: [{ kind: "text", text: "Report content" }] * })) * * // Dynamic artifact * builder.artifact(async ({ context, args }) => ({ * artifactId: `analysis-${context.taskId}`, * name: "Analysis Results", * parts: [{ kind: "data", data: args?.analysisData }] * })) * ``` */ artifact<C extends A.bargs = A.empty>(step: artifactStep<I, C>): AgentFactory<A.inC<A.rep<A.sArtifact, C>>>; artifact<C extends A.bargs = A.empty>(artifact: A.sArtifact): AgentFactory<A.inC<A.rep<A.sArtifact, C>>>; /** * Adds a status update step to the workflow. * * Status steps emit task state updates during execution. Use to communicate * progress, intermediate states, or completion to clients. Supports simple * state strings or full status objects with messages. * * @param step - A status step function, status object, or state string * @returns New builder instance with updated type parameters * * @example * ```typescript * // Simple state string * builder.status("working") * * // Status with message * builder.status(({ args }) => ({ * status: { * state: A2A.TaskState.working, * message: describe.message(`Step ${args?.step} of 5 complete`) * } * })) * * // Mark completion * builder.status(() => ({ * status: { state: A2A.TaskState.completed } * })) * ``` */ status<C extends A.bargs = A.empty>(status: A.sUpdate): AgentFactory<A.inC<A.rep<A.sUpdate, C>>>; status<C extends A.bargs = A.empty>(step: statusStep<I, C>): AgentFactory<A.inC<A.rep<A.sUpdate, C>>>; /** * Adds a task step to the workflow. * * Task steps yield complete A2A task objects. Use when you need full control * over the task representation, including status, artifacts, and history. * Particularly useful for orchestration scenarios or final task construction. * * @param step - A task step function, task object, or string * @returns New builder instance with updated type parameters * * @example * ```typescript * // Simple string (auto-converted to task) * builder.task("Operation completed") * * // Full task object * builder.task(({ context }) => describe.task({ * id: context.taskId, * contextId: context.contextId, * status: { state: A2A.TaskState.completed } * })) * * // With carried args * builder.task(({ context }) => ({ * reply: describe.task({ id: context.taskId }), * args: { completedAt: Date.now() } * })) * ``` */ task<C extends A.bargs = A.empty>(task: A.sTask): AgentFactory<A.inC<A.rep<A.sTask, C>>>; task<C extends A.bargs = A.empty>(step: taskStep<I, C>): AgentFactory<A.inC<A.rep<A.sTask, C>>>; /** * 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<Carry extends A.BaseArgs = { task?: A2A.Task; }>(agent_and_message: { agent: MessageSender; message?: A.sMessage | string; }): AgentFactory<A.inferCarry<A.Reply<A.Stateless<TaskParams>, Carry>>>; /** * 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: describe.AgentCardParams, params?: FactoryParams): AgentFactory<A.EmptyArgs>; } /** * 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 declare const cr8: typeof 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 declare class AgentBuilder extends AgentFactory { constructor(agentCard?: describe.AgentCardParams | string, params?: FactoryParams); } /** * 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 declare function createStepEngine(stepsList: A.Resolved[]): A2A.Engine;