Skip to content

Z-Agent Architecture: Registry vs Class Inheritance Analysis

Executive Summary

This document provides a critical assessment of two architectural approaches for implementing Z-Agents in the ABS Platform: the current registry-based pattern and the proposed class inheritance pattern. The analysis is conducted in the context of modern TypeScript, GraphQL, and software architecture principles.

Recommendation: The class inheritance approach should be adopted as it provides significantly better type safety, developer experience, and alignment with modern development practices while maintaining schema stability.


Current Architecture: Registry-Based Pattern

Overview

The current implementation uses a function-factory pattern with string-based registry lookups:

// Current registry-based approach
const agentLogic = getBSSAgentLogic('payment-agent-v1');
const newState = agentLogic.process(agentParams, currentState, event);

Strengths

  1. Schema Stability: Persisted JSON schema remains unchanged when adding new agent types
  2. Runtime Flexibility: Agents can be dynamically loaded via string-based registry lookup
  3. Separation of Concerns: Business logic separated from instantiation mechanism

Weaknesses

  1. String-based Polymorphism: Goes against modern OO principles
  2. Runtime Type Discovery: Inefficient and error-prone
  3. Weak Type Safety: Everything is JSON with no compile-time validation
  4. Poor Developer Experience: No IntelliSense, runtime errors only
  5. Testing Complexity: Hard to mock and test individual agent logic

Proposed Architecture: Class Inheritance Pattern

Overview

The proposed approach uses abstract base classes with concrete implementations:

// Proposed class inheritance approach
abstract class BaseAgent {
  protected params: any;
  protected state: any;

  abstract process(event: Event): void;
  abstract evaluateSignals(): string[];
}

class PaymentAgent extends BaseAgent {
  process(event: Event) {
    // Payment-specific logic with full type safety
  }

  checkCreditLimit(): Boolean {
    // Payment-specific methods
  }
}

Benefits

  1. Strong Type Safety: Compile-time validation throughout
  2. Excellent Developer Experience: Full IntelliSense and IDE support
  3. Modern OO Principles: Follows SOLID principles
  4. Better Performance: Compile-time resolution vs runtime lookup
  5. Easy Testing: Dependency injection and mocking support

Critical Assessment by Category

1. Type Safety & Developer Experience

Registry Approach:

// Weak typing - everything is JSON
const agent = createZAgent({
  logic: "payment-agent-v1",  // String-based lookup
  params: { /* untyped JSON */ },
  state: { /* untyped JSON */ }
});

// Runtime errors only
agent.process(event); // No compile-time validation

Class Inheritance Approach:

// Strong typing throughout
const agent = new PaymentAgent({
  grace_period_days: 7,
  credit_limit: 1000,
  // TypeScript enforces correct parameter structure
});

// Compile-time validation
agent.processPayment(amount); // Full IntelliSense support

Assessment: Class inheritance provides significantly better type safety and developer experience. Modern TypeScript development heavily favors compile-time validation over runtime discovery.

2. GraphQL Schema Design

Registry Approach:

type ZAgent {
  logic: String!        # String-based type identification
  params: JSON!         # Untyped parameters
  state: JSON!          # Untyped state
}

Class Inheritance Approach:

type PaymentAgent implements BaseAgent {
  params: PaymentAgentParams!    # Strongly typed
  state: PaymentAgentState!      # Strongly typed
  checkCreditLimit(): Boolean!   # Specific methods
}

Assessment: GraphQL's type system is fundamentally designed for strong typing. The registry approach undermines GraphQL's core value proposition of self-documenting, type-safe APIs.

3. Modern Architecture Patterns

Registry Approach Issues: - String-based Polymorphism: Goes against modern OO principles - Runtime Type Discovery: Inefficient and error-prone - Tight Coupling: Registry becomes a bottleneck for all agent types - Testing Complexity: Hard to mock and test individual agent logic

Class Inheritance Benefits: - Compile-time Polymorphism: TypeScript's strength - Dependency Injection: Easy to inject and mock - Single Responsibility: Each agent class has one clear purpose - Open/Closed Principle: Easy to extend without modifying existing code

Assessment: Class inheritance aligns with modern SOLID principles and TypeScript's design philosophy.

4. Performance & Scalability

Registry Approach:

// Runtime lookup overhead
const agentLogic = bssAgentRegistry[logic]; // O(1) but still runtime
if (!agentLogic) throw new Error(`Unknown logic: ${logic}`);

Class Inheritance Approach:

// Compile-time resolution
const agent = new PaymentAgent(params); // Direct instantiation

Assessment: Class inheritance provides better performance through compile-time optimization and eliminates runtime lookup overhead.

5. Schema Evolution & Backward Compatibility

Registry Approach: - ✅ Schema stability (JSON structure unchanged) - ❌ Runtime compatibility issues - ❌ Hard to version agent logic

Class Inheritance Approach: - ✅ Schema stability (can maintain same JSON structure) - ✅ Compile-time compatibility checking - ✅ Easy versioning through class inheritance

Assessment: Class inheritance provides better long-term maintainability while preserving schema stability.

6. GraphQL-Specific Considerations

Registry Approach Problems:

# No way to query specific agent capabilities
query {
  zAgent(id: "123") {
    logic    # Just a string
    params   # Untyped JSON
    # Can't query payment-specific fields
  }
}

Class Inheritance Benefits:

# Type-safe, specific queries
query {
  paymentAgent(id: "123") {
    checkCreditLimit()
    availableCredits
    # Full type safety and IntelliSense
  }
}

Assessment: GraphQL's introspection and type system are fundamentally incompatible with the registry approach.

7. Modern Development Workflow

Registry Approach: - ❌ No IDE autocomplete for agent-specific methods - ❌ Runtime errors for missing agent types - ❌ Difficult refactoring (string-based references) - ❌ Poor testability

Class Inheritance Approach: - ✅ Full IDE support and IntelliSense - ✅ Compile-time error detection - ✅ Easy refactoring with TypeScript tools - ✅ Excellent testability with dependency injection


Implementation Strategy

Hybrid Migration Approach

Keep the registry as a runtime compatibility layer for existing persisted data, but implement new agents using class inheritance:

// Hybrid approach during migration
class AgentFactory {
  static createFromConfig(config: AgentConfig): BaseAgent {
    // Try class-based first
    if (config.type === 'payment') {
      return new PaymentAgent(config.params);
    }

    // Fall back to registry for legacy agents
    return createLegacyAgent(config);
  }
}

Migration Phases

  1. Phase 1: Create abstract base class and concrete agent classes
  2. Phase 2: Implement registry that maps string types to classes
  3. Phase 3: Update Service_Plan to use new class-based agents
  4. Phase 4: Migrate existing agent logic from registry to individual classes
  5. Phase 5: Remove old registry-based logic

Schema Comparison

Registry Approach (Current)

type ZAgent {
  id: ID!
  name: String!
  description: String
  logic: String!                # "quota-agent-v1", "payment-agent-v1", etc.
  params: JSON!                 # Untyped configuration parameters
  state: JSON!                  # Untyped internal state
  created_at: DateTime!
  updated_at: DateTime!
}

type ProcessZAgentPoolResult {
  service_plan_id: ID!
  stimulus: String!
  processed_at: DateTime!
  agent_responses: [ZAgentResponse!]!
  generated_signals: [String!]!
  metadata: JSON
}

Class Inheritance Approach (Proposed)

# Abstract base class
type BaseAgent {
  id: ID!
  name: String!
  description: String
  agent_type: String!              # "payment", "quota", "inventory", etc.
  version: String!                 # Agent version for compatibility
  params: JSON!                    # Configuration parameters (strongly typed in concrete classes)
  state: JSON!                     # Persistent internal state (strongly typed in concrete classes)
  created_at: DateTime!
  updated_at: DateTime!

  # Common methods available on all agents
  processEvent(event: Event!): AgentResponse!
  evaluateSignals(): [String!]!
  getState(): JSON!
  setState(state: JSON!): Boolean!
}

# Concrete agent types with specific parameter and state schemas
type PaymentAgent implements BaseAgent {
  id: ID!
  name: String!
  description: String
  agent_type: String!
  version: String!
  params: PaymentAgentParams!      # Strongly typed parameters
  state: PaymentAgentState!        # Strongly typed state
  created_at: DateTime!
  updated_at: DateTime!

  # Payment-specific methods
  processEvent(event: Event!): AgentResponse!
  evaluateSignals(): [String!]!
  getState(): JSON!
  setState(state: JSON!): Boolean!

  # Payment-specific business logic
  checkCreditLimit(): Boolean!
  calculateLateFees(): Float!
  processPayment(amount: Float!): PaymentResult!
}

# Strongly typed parameter and state schemas
type PaymentAgentParams {
  grace_period_days: Int!
  late_fee_percentage: Float!
  max_past_due_days: Int!
  credit_limit: Float!
  auto_payment_enabled: Boolean!
  event_kind: String!
}

type PaymentAgentState {
  account_balance: Float!
  available_credits: Float!
  overdue_balance: Float!
  last_payment: DateTime
  credits_used: Float!
  total_late_fees: Float!
}

Conclusion

Critical Recommendation

The registry approach is fundamentally flawed for modern TypeScript/GraphQL development because it:

  1. Undermines TypeScript's core value of compile-time type safety
  2. Violates GraphQL's type system principles
  3. Creates runtime fragility in a compile-time world
  4. Increases cognitive load for developers
  5. Makes testing and maintenance harder

The class inheritance approach should be adopted because it:

  1. Leverages TypeScript's strengths fully
  2. Aligns with GraphQL's type system naturally
  3. Provides better developer experience and productivity
  4. Enables modern development practices (TDD, dependency injection, etc.)
  5. Maintains schema stability while improving type safety

Final Assessment

In the context of modern TypeScript and GraphQL, the class inheritance approach is objectively superior and should be the target architecture. The registry approach should be viewed as a legacy pattern that needs migration.

The benefits of class inheritance far outweigh the migration effort, and the hybrid approach allows for gradual migration without breaking existing functionality.


References