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