Build/Agents

Calling Other Agents

Build multi-agent systems with type-safe agent-to-agent communication

Break complex tasks into focused, reusable agents that communicate with type safety. Instead of building one large agent, create specialized agents that each handle a single responsibility.

Basic Usage

Import and call other agents directly:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import enrichmentAgent from '@agent/enrichment';
 
const coordinator = createAgent('Coordinator', {
  schema: {
    input: s.object({ text: s.string() }),
    output: s.object({ result: s.string() }),
  },
  handler: async (ctx, input) => {
    // Call another agent by importing it
    const enriched = await enrichmentAgent.run({
      text: input.text,
    });
 
    return { result: enriched.enrichedText };
  },
});
 
export default coordinator;

When both agents have schemas, TypeScript validates the input and infers the output type automatically.

Type Safety

Define schemas on all agents to enable full type inference. TypeScript will validate that inputs match expected types and provide autocomplete for outputs.

Communication Patterns

Sequential Execution

Process data through a series of agents where each step depends on the previous result:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import validatorAgent from '@agent/validator';
import enrichmentAgent from '@agent/enrichment';
import analysisAgent from '@agent/analysis';
 
const pipeline = createAgent('Pipeline', {
  schema: {
    input: s.object({ rawData: s.string() }),
    output: s.object({ processed: s.any() }),
  },
  handler: async (ctx, input) => {
    // Each step depends on the previous result
    const validated = await validatorAgent.run({
      data: input.rawData,
    });
 
    const enriched = await enrichmentAgent.run({
      data: validated.cleanData,
    });
 
    const analyzed = await analysisAgent.run({
      data: enriched.enrichedData,
    });
 
    return { processed: analyzed };
  },
});
 
export default pipeline;

Errors propagate automatically. If validatorAgent throws, subsequent agents never execute.

Parallel Execution

Run multiple agents simultaneously when their operations are independent:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import webSearchAgent from '@agent/web-search';
import databaseAgent from '@agent/database';
import vectorSearchAgent from '@agent/vector-search';
 
const searchAgent = createAgent('Search', {
  schema: {
    input: s.object({ query: s.string() }),
    output: s.object({ results: s.array(s.any()) }),
  },
  handler: async (ctx, input) => {
    // Execute all searches in parallel
    const [webResults, dbResults, vectorResults] = await Promise.all([
      webSearchAgent.run({ query: input.query }),
      databaseAgent.run({ query: input.query }),
      vectorSearchAgent.run({ query: input.query }),
    ]);
 
    return {
      results: [...webResults.items, ...dbResults.items, ...vectorResults.items],
    };
  },
});
 
export default searchAgent;

If each agent takes 1 second, parallel execution completes in 1 second instead of 3.

Background Execution

Use ctx.waitUntil() for fire-and-forget operations that continue after returning a response:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import analyticsAgent from '@agent/analytics';
 
const processor = createAgent('Processor', {
  schema: {
    input: s.object({ data: s.any() }),
    output: s.object({ status: s.string(), id: s.string() }),
  },
  handler: async (ctx, input) => {
    const id = crypto.randomUUID();
 
    // Start background processing
    ctx.waitUntil(async () => {
      await analyticsAgent.run({
        event: 'processed',
        data: input.data,
      });
      ctx.logger.info('Background processing completed', { id });
    });
 
    // Return immediately
    return { status: 'accepted', id };
  },
});
 
export default processor;

When to Use Background Execution

Use ctx.waitUntil() for analytics, logging, notifications, or any operation where the caller doesn't need the result.

Conditional Routing

Use an LLM to classify intent and route to the appropriate agent:

import supportAgent from '@agent/support';
import salesAgent from '@agent/sales';
import technicalAgent from '@agent/technical';
 
handler: async (ctx, input) => {
  // Classify with a fast model, using Groq (via AI Gateway)
  const { object: intent } = await generateObject({
    model: groq('llama-3.3-70b'),
    schema: z.object({
      agentType: z.enum(['support', 'sales', 'technical']),
    }),
    prompt: input.message,
  });
 
  // Route based on classification
  switch (intent.agentType) {
    case 'support':
      return supportAgent.run(input);
    case 'sales':
      return salesAgent.run(input);
    case 'technical':
      return technicalAgent.run(input);
  }
}

See Full Example below for a complete implementation with error handling and logging.

Orchestrator Pattern

An orchestrator is a coordinator agent that delegates work to specialized agents and combines their results. This pattern is useful for:

  • Multi-step content pipelines (generate → evaluate → refine)
  • Parallel data gathering from multiple sources
  • Workflows requiring different expertise (writer + reviewer + formatter)
import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
import writerAgent from '@agent/writer';
import evaluatorAgent from '@agent/evaluator';
 
const orchestrator = createAgent('Orchestrator', {
  schema: {
    input: s.object({ topic: s.string() }),
    output: s.object({ content: s.string(), score: s.number() }),
  },
  handler: async (ctx, input) => {
    // Step 1: Generate content
    const draft = await writerAgent.run({ prompt: input.topic });
 
    // Step 2: Evaluate quality
    const evaluation = await evaluatorAgent.run({
      content: draft.text,
    });
 
    // Step 3: Return combined result
    return { content: draft.text, score: evaluation.score };
  },
});
 
export default orchestrator;

Common Pattern

The orchestrator pattern is common in AI workflows where you want to separate concerns (generation, evaluation, formatting) into focused agents. You'll see this pattern in many of our multi-agent examples.

Public Agents

Cross-Project Limitation

Agent imports only work within the same project. To call agents in other projects or organizations, use fetch() with the agent's public URL.

Call public agents using standard HTTP:

import { createAgent } from '@agentuity/runtime';
import { s } from '@agentuity/schema';
 
const agent = createAgent('External Caller', {
  schema: {
    input: s.object({ query: s.string() }),
    output: s.object({ result: s.string() }),
  },
  handler: async (ctx, input) => {
    try {
      const response = await fetch(
        'https://agentuity.ai/api/agent-id-here',
        {
          method: 'POST',
          body: JSON.stringify({ query: input.query }),
          headers: { 'Content-Type': 'application/json' },
        }
      );
 
      if (!response.ok) {
        throw new Error(`Public agent returned ${response.status}`);
      }
 
      const data = await response.json();
      return { result: data.response };
    } catch (error) {
      ctx.logger.error('Public agent call failed', { error });
      throw error;
    }
  },
});
 
export default agent;

Error Handling

Cascading Failures

By default, errors propagate through the call chain:

import validatorAgent from '@agent/validator';
import processorAgent from '@agent/processor';
 
const pipeline = createAgent('Pipeline', {
  handler: async (ctx, input) => {
    // If validatorAgent throws, execution stops here
    const validated = await validatorAgent.run(input);
 
    // This never executes if validation fails
    const processed = await processorAgent.run(validated);
 
    return processed;
  },
});

This is the recommended pattern for critical operations where later steps cannot proceed without earlier results.

Graceful Degradation

For optional operations, catch errors and continue:

import enrichmentAgent from '@agent/enrichment';
import processorAgent from '@agent/processor';
 
const resilientProcessor = createAgent('Resilient Processor', {
  handler: async (ctx, input) => {
    let enrichedData = input.data;
 
    // Try to enrich, but continue if it fails
    try {
      const enrichment = await enrichmentAgent.run({
        data: input.data,
      });
      enrichedData = enrichment.data;
    } catch (error) {
      ctx.logger.warn('Enrichment failed, using original data', {
        error: error instanceof Error ? error.message : String(error),
      });
    }
 
    // Process with enriched data (or original if enrichment failed)
    return await processorAgent.run({ data: enrichedData });
  },
});

Retry Pattern

Implement retry logic for unreliable operations:

import externalServiceAgent from '@agent/external-service';
 
async function callWithRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3,
  delayMs: number = 1000
): Promise<T> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;
 
      // Exponential backoff
      const delay = delayMs * Math.pow(2, attempt - 1);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
  throw new Error('Retry failed');
}
 
const retryHandler = createAgent('Retry Handler', {
  handler: async (ctx, input) => {
    const result = await callWithRetry(() =>
      externalServiceAgent.run(input)
    );
    return result;
  },
});

Partial Failure Handling

Handle mixed success/failure results with Promise.allSettled():

import processingAgent from '@agent/processing';
 
const batchProcessor = createAgent('Batch Processor', {
  handler: async (ctx, input) => {
    const results = await Promise.allSettled(
      input.items.map((item) => processingAgent.run({ item }))
    );
 
    const successful = results
      .filter((r) => r.status === 'fulfilled')
      .map((r) => r.value);
 
    const failed = results
      .filter((r) => r.status === 'rejected')
      .map((r) => r.reason);
 
    if (failed.length > 0) {
      ctx.logger.warn('Some operations failed', { failedCount: failed.length });
    }
 
    return { successful, failedCount: failed.length };
  },
});

Best Practices

Keep Agents Focused

Each agent should have a single, well-defined responsibility:

// Good: focused agents
const validatorAgent = createAgent('Validator', { /* validates data */ });
const enrichmentAgent = createAgent('Enrichment', { /* enriches data */ });
const analysisAgent = createAgent('Analysis', { /* analyzes data */ });
 
// Bad: monolithic agent
const megaAgent = createAgent('MegaAgent', {
  handler: async (ctx, input) => {
    // Validates, enriches, analyzes all in one place
  },
});

Focused agents are easier to test, reuse, and maintain.

Use Schemas for Type Safety

Define schemas on all agents for type-safe communication. See Creating Agents to learn more about using schemas.

import sourceAgent from '@agent/source';
 
// Source agent with output schema
const source = createAgent('Source', {
  schema: {
    output: z.object({
      data: z.string(),
      metadata: z.object({ timestamp: z.string() }),
    }),
  },
  handler: async (ctx, input) => {
    return {
      data: 'result',
      metadata: { timestamp: new Date().toISOString() },
    };
  },
});
 
// Consumer agent - TypeScript validates the connection
const consumer = createAgent('Consumer', {
  handler: async (ctx, input) => {
    const result = await source.run({});
    // TypeScript knows result.data and result.metadata.timestamp exist
    return { processed: result.data };
  },
});

Leverage Shared Context

Agent calls share the same session context:

import processingAgent from '@agent/processing';
 
const coordinator = createAgent('Coordinator', {
  handler: async (ctx, input) => {
    // Store data in thread state
    ctx.thread.state.set('userId', input.userId);
 
    // Called agents can access the same thread state
    const result = await processingAgent.run(input);
 
    // All agents share sessionId
    ctx.logger.info('Processing complete', { sessionId: ctx.sessionId });
 
    return result;
  },
});

Use this for tracking context, sharing auth data, and maintaining conversation state.

Full Example

A customer support router that combines multiple patterns: conditional routing, graceful degradation, and background analytics.

import { createAgent } from '@agentuity/runtime';
import { generateObject } from 'ai';
import { groq } from '@ai-sdk/groq';
import { z } from 'zod';
import supportAgent from '@agent/support';
import salesAgent from '@agent/sales';
import billingAgent from '@agent/billing';
import generalAgent from '@agent/general';
import analyticsAgent from '@agent/analytics';
 
const IntentSchema = z.object({
  agentType: z.enum(['support', 'sales', 'billing', 'general']),
  confidence: z.number().min(0).max(1),
  reasoning: z.string(),
});
 
const router = createAgent('Customer Router', {
  schema: {
    input: z.object({ message: z.string() }),
    output: z.object({
      response: z.string(),
      handledBy: z.string(),
    }),
  },
  handler: async (ctx, input) => {
    let intent: z.infer<typeof IntentSchema>;
    let handledBy = 'general';
 
    // Classify intent with graceful degradation
    try {
      const result = await generateObject({
        model: groq('llama-3.3-70b'),
        schema: IntentSchema,
        system: 'Classify the customer message by intent.',
        prompt: input.message,
        temperature: 0,
      });
      intent = result.object;
 
      ctx.logger.info('Intent classified', {
        type: intent.agentType,
        confidence: intent.confidence,
      });
    } catch (error) {
      // Fallback to general agent if classification fails
      ctx.logger.warn('Classification failed, using fallback', {
        error: error instanceof Error ? error.message : String(error),
      });
      intent = { agentType: 'general', confidence: 0, reasoning: 'fallback' };
    }
 
    // Route to specialist agent
    let response: string;
    try {
      switch (intent.agentType) {
        case 'support':
          const supportResult = await supportAgent.run({
            message: input.message,
            context: intent.reasoning,
          });
          response = supportResult.response;
          handledBy = 'support';
          break;
 
        case 'sales':
          const salesResult = await salesAgent.run({
            message: input.message,
            context: intent.reasoning,
          });
          response = salesResult.response;
          handledBy = 'sales';
          break;
 
        case 'billing':
          const billingResult = await billingAgent.run({
            message: input.message,
            context: intent.reasoning,
          });
          response = billingResult.response;
          handledBy = 'billing';
          break;
 
        default:
          const generalResult = await generalAgent.run({
            message: input.message,
          });
          response = generalResult.response;
          handledBy = 'general';
      }
    } catch (error) {
      ctx.logger.error('Specialist agent failed', { error, intent });
      response = 'I apologize, but I encountered an issue. Please try again.';
      handledBy = 'error';
    }
 
    // Log analytics in background (doesn't block response)
    ctx.waitUntil(async () => {
      await analyticsAgent.run({
        event: 'customer_interaction',
        intent: intent.agentType,
        confidence: intent.confidence,
        handledBy,
        sessionId: ctx.sessionId,
      });
    });
 
    return { response, handledBy };
  },
});
 
export default router;

This example combines several patterns:

  • Use an LLM to classify intent and route to specialist agents
  • Handle failures gracefully: fallback to general agent if classification fails, friendly error message if specialists fail
  • Log analytics in the background with waitUntil() so the response isn't delayed

Next Steps

  • State Management: Share data across agent calls with thread and session state
  • Evaluations: Add quality checks to your agent workflows

Need Help?

Join our DiscordCommunity for assistance or just to hang with other humans building agents.

Send us an email at hi@agentuity.com if you'd like to get in touch.

Please Follow us on

If you haven't already, please Signup for your free account now and start building your first agent!