@artinet/sdk
Version:
A TypeScript SDK for building collaborative AI agents.
489 lines (488 loc) • 15.6 kB
JavaScript
/**
* 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;
};
}