The AMU (Adapter-Metadata-Usecases) is a general purpose pattern used throughout Agentic Team OS, influenced by hexagonal architecture and ports and adapters pattern. It provides a clear separation of concerns and promotes maintainable, testable code.

Pattern Overview

The pattern consists of three main components:

1. Adapters

Adapters handle external interactions and infrastructure concerns:

This is split into primary and secondary adapters.

  • Primary adapters are for handling incoming requests to the unit. For example a Lambda handler or DynamoDB stream handler.
  • Secondary adapters are for handling outgoing requests to external services. For example an OpenAI client or a database client.

Primary Adapter

Here is an example of a primary adapter in the Orchestrator for an Agent that handles generating a value strategy. for a saas founder.

/**
 * Lambda function that handles API Gateway requests for the value strategy service
 * Demonstrates the primary adapter pattern by:
 * 1. Authenticating request via SaaS Identity
 * 2. Enriching request with generated IDs
 * 3. Receiving external request
 * 4. Transforming to internal format 
 * 5. Calling usecase
 * 6. Transforming response back to HTTP
 */
async function valueStrategyLambdaHandler(apiGatewayEvent) {
  try {
    // 1. Authentication & Authorization
    // Verify JWT token and extract claims
    const token = apiGatewayEvent.headers.Authorization;
    const identityService = new SaaSIdentityService();
    const userClaims = await identityService.validateToken(token);
    
    if (!userClaims.isValid) {
      return {
        statusCode: 401,
        body: { message: 'Unauthorized' }
      };
    }

    // 2. Request Enrichment
    // Generate unique IDs for tracking and correlation
    const enrichmentService = new EnrichmentService();
    const orderId = enrichmentService.generateOrderId(); // e.g. "ord_123xyz..."
    const deliverableId = enrichmentService.generateDeliverableId(); // e.g. "del_456abc..."
    
    // Optional: Add metadata like timestamp, request source, etc
    const metadata = {
      requestTimestamp: new Date().toISOString(),
      sourceIp: apiGatewayEvent.requestContext.identity.sourceIp,
      userAgent: apiGatewayEvent.requestContext.identity.userAgent
    };

    // 3. Extract and validate input from HTTP request
    // Adapts API Gateway event into domain input format
    const requestBody = parseRequestBody(apiGatewayEvent);
    const validatedInput = {
      orderId: orderId, // Generated ID
      keyId: userClaims.keyId, // From SaaS identity
      userId: userClaims.userId, // From SaaS identity
      deliverableId: deliverableId, // Generated ID
      agentId: requestBody.agentId,
      deliverableName: requestBody.deliverableName,
      applicationIdea: requestBody.applicationIdea,
      idealCustomer: requestBody.idealCustomer,
      problem: requestBody.problem,
      solution: requestBody.solution,
      metadata: metadata // Optional enrichment data
    };

    // 4. Call domain usecase with validated input
    // This is where we cross the boundary from external to internal
    const result = await publishValueStrategyUseCase(validatedInput);

    // 5. Transform usecase result to HTTP response
    // Adapts domain result back to API format
    return {
      statusCode: 200,
      body: {
        orderId: result.orderId,
        deliverableId: result.deliverableId,
        status: result.orderStatus,
        createdAt: result.orderCreatedAt,
        deliverableName: result.deliverableName
      }
    };

  } catch (error) {
    // 6. Error handling and logging
    // Transforms domain errors to HTTP error responses
    console.error('Lambda handler error:', error);
    
    // Map different error types to appropriate HTTP status codes
    if (error.name === 'ValidationError') {
      return {
        statusCode: 400,
        body: { message: 'Invalid request parameters' }
      };
    }
    
    return {
      statusCode: 500,
      body: { message: 'Internal server error' }
    };
  }
}

Key Points of Primary Adapter Pattern:

  • Handles all Protocol specific logic
  • Integrates with SaaS Identity for auth
  • Enriches the request with metadata before passing it to the usecase
  • Handles response formatting and errors
  • Keeps infrastructure concerns separate from business logic

Secondary Adapter

Secondary adapters handle external integrations like AI services, databases, and third-party APIs. Here’s an example of a secondary adapter for OpenAI:

/**
 * Example of a secondary adapter for OpenAI integration
 * Demonstrates the secondary adapter pattern by:
 * 1. Initializing external client
 * 2. Handling external service configuration
 * 3. Managing service interactions
 * 4. Error handling and retries
 * 5. Response transformation
 */
export const createValueStrategy = async (input: RequestValueStrategyInput): Promise<Deliverable> => {
  try {
    // Initialize external service client
    const assistant = await client.beta.assistants.create({
      name: "Value Strategist",
      instructions: systemPrompt,
      model: "gpt-4",
      response_format: responseFormat,
      tools: [/* tool configurations */]
    });

    // Create session/context
    const thread = await client.beta.threads.create();

    // Transform and send input
    await client.beta.threads.messages.create(thread.id, {
      role: "user",
      content: `Please create a value proposition for:
        Application Idea: ${input.applicationIdea}
        Ideal Customer: ${input.idealCustomer}
        Problem: ${input.problem}
        Solution: ${input.solution}`
    });

    // Execute external service call
    const run = await client.beta.threads.runs.create(thread.id, {
      assistant_id: assistant.id
    });

    // Handle async completion and status checks
    const completedRun = await waitForRunCompletion(client, thread.id, run.id);
    
    // Transform and validate response
    const messages = await client.beta.threads.messages.list(thread.id);
    const content = parseAndValidateResponse(messages);
    
    // Cleanup resources
    await cleanup(assistant.id, thread.id);

    return content;
  } catch (error) {
    console.error('Error in external service:', error);
    throw error;
  }
};

// Add retry wrapper for resilience
export const runValueStrategy = withRetry(createValueStrategy, {
  retries: 3,
  delay: 1000,
  onRetry: (error) => console.warn('Retrying due to error:', error)
});

Key Points of Secondary Adapter Pattern:

  • Encapsulates external service integration details
  • Handles service-specific configuration
  • Manages connection lifecycle
  • Implements retry and error handling
  • Transforms data between domain and external formats

2. Metadata

Metadata is used to glue the adapters and usecases together. It defines the domain models, types, and interfaces:

// System prompts/instructions
export const systemPrompt = () => `
  You are an expert value strategist. Your order is to create 
  a detailed one-page value proposition based on the provided 
  application idea, target customer, problem, and proposed solution.
`;

// Domain schemas and types
export const ValueStrategySchema = z.object({
  deliverableName: z.string(),
  sections: z.object({
    applicationIdea: z.object({
      id: z.string(),
      label: z.string(),
      type: z.literal('text'),
      description: z.string().optional(),
      data: z.string()
    }),
    valueProposition: z.object({
      id: z.string(),
      label: z.string(),
      type: z.literal('text'),
      description: z.string().optional(),
      data: z.string()
    }),
    benefitBreakdown: z.object({
      id: z.string(),
      label: z.string(),
      type: z.literal('list'),
      description: z.string().optional(),
      data: z.array(z.string())
    })
    // ... other sections
  })
});

// Base schemas for requests/responses
export const BasePayloadSchema = z.object({
  userId: z.string(),
  orderId: z.string(),
  deliverableId: z.string(),
  deliverableName: z.string(),
  agentId: z.string()
});

// Input/Output schemas
export const RequestValueStrategyInputSchema = BasePayloadSchema.extend({
  applicationIdea: z.string(),
  idealCustomer: z.string(),
  problem: z.string(),
  solution: z.string()
});

export const DeliverableSchema = z.object({
  deliverableContent: ValueStrategySchema
});

// Type exports
export type RequestValueStrategyInput = z.infer<typeof RequestValueStrategyInputSchema>;
export type Deliverable = z.infer<typeof DeliverableSchema>;

Key Points of Metadata Pattern:

  • Defines system prompts and instructions
  • Specifies data schemas and validation rules
  • Declares domain types and interfaces
  • Establishes contract between adapters and usecases
  • Provides type safety and runtime validation

3. Usecases

Usecases implement the business logic and orchestrate the flow between adapters. They handle:

  • Business rules and domain logic
  • Flow coordination between adapters
  • Error handling and logging
  • Data transformation and validation

Here’s an example usecase that generates a value strategy:

export const createValueStrategyUsecase = async (input: RequestValueStrategyInput): Promise<Message> => {
  console.log("--- Create Value Strategy Usecase ---");
  try {
    // Generate strategy content using OpenAI adapter
    const deliverableContent = await runValueStrategy(input);

    // Transform result into deliverable format
    const deliverable: DeliverableDTO = {
      userId: input.userId,
      orderId: input.orderId,
      deliverableId: input.deliverableId,
      deliverableName: input.deliverableName,
      agentId: input.agentId,
      ...deliverableContent
    };

    // Store result using repository adapter
    await deliverableRepository.saveDeliverable(deliverable);

    // Return success message
    return {
      message: "Value strategy created successfully"
    };

  } catch (error) {
    console.error('Error generating value strategy:', error);
    throw new Error('Failed to generate value strategy');
  }
};

Key Points of Usecase Pattern:

  • Pure business logic with no infrastructure concerns
  • Coordinates between multiple adapters (e.g., OpenAI and database)
  • Handles error cases and logging
  • Transforms data between adapter formats
  • Returns standardized response types

Pattern Benefits

  1. Separation of Concerns

    • Clear boundaries between components
    • Easier to maintain and modify
    • Better testability
  2. Dependency Inversion

    • Business logic depends on abstractions
    • Infrastructure details are isolated
    • Easier to swap implementations
  3. Testability

    • Business logic can be tested in isolation
    • Easy to mock external dependencies
    • Clear component boundaries

Best Practices

  1. Keep Components Focused

    • Adapters handle external concerns
    • Metadata defines structures
    • Usecases contain business logic
  2. Dependency Injection

    • Inject dependencies through constructors
    • Use interfaces for dependencies
    • Follow IoC principles
  3. Error Handling

    • Define domain-specific errors
    • Handle errors at appropriate levels
    • Maintain error boundaries
  4. Testing Strategy

    • Unit test usecases in isolation
    • Mock adapters in tests
    • Integration test full flows