Skip to content

Architecture Overview

This document explains the internal architecture of Helix Agents for contributors and advanced users who want to understand how the framework works.

Design Philosophy

Helix Agents follows these core principles:

  1. Interface-First Design - Core abstractions are interfaces, enabling multiple implementations
  2. Pure Functions for Logic - Orchestration logic is pure functions with no side effects
  3. Runtime Agnosticism - Agent definitions work across all runtimes unchanged
  4. Composability - Every component can be swapped or extended

Package Architecture

┌──────────────────────────────────────────────────────────────────────────┐
│                              Applications                                 │
└───────────────────────────────────┬──────────────────────────────────────┘

┌───────────────────────────────────┴──────────────────────────────────────┐
│                           @helix-agents/sdk                               │
│                    (Umbrella - core + memory + js)                        │
└───────────────────────────────────┬──────────────────────────────────────┘

        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
        ▼                           ▼                           ▼
┌───────────────┐           ┌───────────────┐           ┌───────────────┐
│  runtime-js   │           │runtime-temporal│          │runtime-cloudflare│
│               │           │               │           │               │
│  In-process   │           │  Workflows +  │           │  Workers +    │
│  execution    │           │  Activities   │           │  Workflows    │
└───────┬───────┘           └───────┬───────┘           └───────┬───────┘
        │                           │                           │
        └───────────────────────────┼───────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────┐
│                            @helix-agents/core                             │
│                                                                           │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐     │
│  │   Types     │  │Orchestration│  │    State    │  │   Stream    │     │
│  │             │  │             │  │             │  │             │     │
│  │ Agent       │  │ init        │  │ Immer       │  │ Filters     │     │
│  │ Tool        │  │ step        │  │ Tracker     │  │ Projections │     │
│  │ State       │  │ messages    │  │ Patches     │  │ Handler     │     │
│  │ Stream      │  │ stop        │  │             │  │             │     │
│  └─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘     │
│                                                                           │
│  ┌─────────────┐  ┌─────────────┐                                        │
│  │  Interfaces │  │    LLM      │                                        │
│  │             │  │             │                                        │
│  │ StateStore  │  │ Adapter     │                                        │
│  │ StreamMgr   │  │ Mock        │                                        │
│  │ LLMAdapter  │  │ StopReason  │                                        │
│  └─────────────┘  └─────────────┘                                        │
└──────────────────────────────────────────────────────────────────────────┘

        ┌───────────────────────────┼───────────────────────────┐
        │                           │                           │
┌───────┴───────┐           ┌───────┴───────┐           ┌───────┴───────┐
│ store-memory  │           │  store-redis  │           │store-cloudflare│
│               │           │               │           │               │
│  InMemory     │           │    Redis      │           │   D1 + DO     │
│  State/Stream │           │  State/Stream │           │ State/Stream  │
└───────────────┘           └───────────────┘           └───────────────┘

┌───────────────┐           ┌───────────────┐
│  llm-vercel   │           │    ai-sdk     │
│               │           │               │
│  Vercel AI    │           │  Frontend     │
│  SDK Adapter  │           │  Integration  │
└───────────────┘           └───────────────┘

Core Module Structure

Types (/types)

Pure type definitions with minimal runtime code:

  • agent.ts - Agent configuration, LLM config
  • tool.ts - Tool definition, sub-agent tools, finish tool
  • state.ts - Agent state, messages, thinking content
  • stream.ts - Stream chunks, all event types
  • runtime.ts - Step results, stop reasons, execution types
  • executor.ts - Executor interface
  • logger.ts - Logging interface

Orchestration (/orchestration)

Pure functions that implement agent logic:

orchestration/
├── state-operations.ts   # initializeAgentState, buildEffectiveTools
├── message-builder.ts    # createAssistantMessage, createToolResultMessage
├── step-processor.ts     # planStepProcessing
└── stop-checker.ts       # shouldStopExecution, determineFinalStatus

These functions are used by all runtimes to ensure consistent behavior.

State (/state)

State tracking with Immer:

state/
└── immer-tracker.ts      # ImmerStateTracker, RFC 6902 patches

LLM (/llm)

LLM adapter interface and utilities:

llm/
├── adapter.ts            # LLMAdapter interface
├── mock-adapter.ts       # MockLLMAdapter for testing
└── stop-reason.ts        # Stop reason mapping utilities

Store (/store)

Store interfaces (no implementations):

store/
├── state-store.ts        # StateStore interface
├── stream-manager.ts     # StreamManager, Writer, Reader interfaces
└── stream-events.ts      # Wire format for stream transport

Stream (/stream)

Stream utilities:

stream/
├── filters.ts            # filterByType, excludeTypes, etc.
├── state-streamer.ts     # CustomStateStreamer
├── state-projection.ts   # StateProjection, StreamProjector
└── resumable-handler.ts  # HTTP endpoint helpers

Data Flow

Agent Execution Flow

User Input


┌─────────────────────────────────────────────────────────┐
│ 1. Initialize                                           │
│    initializeAgentState(agent, input, runId, streamId)  │
│    ├── Parse input (message + optional state)           │
│    ├── Apply state schema defaults                      │
│    ├── Create AgentState structure                      │
│    └── Add initial user message                         │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 2. Build Messages                                       │
│    buildMessagesForLLM(messages, systemPrompt, state)   │
│    ├── Resolve dynamic system prompt                    │
│    └── Prepend system message                           │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 3. Call LLM                                             │
│    llmAdapter.generateStep(input, callbacks)            │
│    ├── Convert messages to provider format              │
│    ├── Call provider API with streaming                 │
│    ├── Emit streaming events (text, tools, thinking)    │
│    └── Return StepResult                                │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 4. Process Step                                         │
│    planStepProcessing(stepResult, options)              │
│    ├── Check for __finish__ tool call                   │
│    ├── Plan assistant message                           │
│    ├── Identify pending tool calls                      │
│    ├── Determine status update                          │
│    └── Return StepProcessingPlan                        │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 5. Execute Tools (if any)                               │
│    For each pendingToolCall:                            │
│    ├── Execute tool with ToolContext                    │
│    ├── Collect state patches                            │
│    ├── Create tool result message                       │
│    └── Emit tool_end stream event                       │
└─────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│ 6. Check Stop Condition                                 │
│    shouldStopExecution(result, stepCount, config)       │
│    ├── Check terminal step types                        │
│    ├── Check max steps                                  │
│    └── Check custom stopWhen                            │
└─────────────────────────────────────────────────────────┘

    ├── Continue? → Go to step 2


┌─────────────────────────────────────────────────────────┐
│ 7. Finalize                                             │
│    ├── Apply final status update                        │
│    ├── End stream                                       │
│    └── Return result                                    │
└─────────────────────────────────────────────────────────┘

Stream Event Flow

LLM Response

    ├── Text chunk ──────────► text_delta { content }

    ├── Thinking chunk ──────► thinking { content, isComplete }

    ├── Tool call start ─────► tool_start { toolCallId, toolName, arguments }
    │                               │
    │                               ▼
    │                         Tool Execution
    │                               │
    │                               ├── State mutation ──► state_patch { patches }
    │                               │
    │                               ├── Custom event ────► custom { eventName, data }
    │                               │
    │                               ▼
    │                         tool_end { toolCallId, result }

    ├── Sub-agent call ──────► subagent_start { subAgentId, agentType }
    │                               │
    │                               ▼
    │                         Sub-Agent Execution (recursive)
    │                               │
    │                               ▼
    │                         subagent_end { subAgentId, result }

    ├── Error ───────────────► error { error, recoverable }

    └── Structured output ───► output { output }

Extension Points

Custom Runtime

Implement the execution loop using core orchestration functions:

typescript
// Use pure functions for logic
import {
  initializeAgentState,
  buildMessagesForLLM,
  buildEffectiveTools,
  planStepProcessing,
  shouldStopExecution,
} from '@helix-agents/core';

// Implement AgentExecutor interface
class MyCustomRuntime implements AgentExecutor {
  async execute(agent, input) {
    // Use orchestration functions
    const state = initializeAgentState({ agent, input, runId, streamId });
    // ... execution loop
  }
}

Custom Store

Implement the store interfaces:

typescript
import type { StateStore, StreamManager } from '@helix-agents/core';

class MyStateStore implements StateStore {
  async save(state) { ... }  // runId is inside state object
  async load(runId) { ... }
  async exists(runId) { ... }
  async updateStatus(runId, status) { ... }
  async getMessages(runId, options) { ... }
}

class MyStreamManager implements StreamManager {
  async createWriter(streamId, agentId, agentType) { ... }
  async createReader(streamId) { ... }
  async endStream(streamId, output?) { ... }
  async failStream(streamId, error) { ... }
  async getInfo(streamId) { ... }
}

Custom LLM Adapter

Implement the LLM adapter interface:

typescript
import type { LLMAdapter, LLMGenerateInput, StepResult } from '@helix-agents/core';

class MyLLMAdapter implements LLMAdapter {
  async generateStep(input: LLMGenerateInput): Promise<StepResult> {
    // Call your LLM provider
    // Stream events via callbacks in input
    // Return StepResult
  }
}

Key Design Decisions

Why Pure Functions?

Orchestration uses pure functions because:

  1. Testability - Easy to unit test without mocking I/O
  2. Reusability - Same logic works across all runtimes
  3. Determinism - Required for Temporal workflow replay
  4. Clarity - Clear separation of logic and effects

Why Interfaces?

Store and adapter interfaces enable:

  1. Swappable Implementations - Dev vs production stores
  2. Testing - In-memory mocks for unit tests
  3. Platform Adaptation - Different stores for different platforms
  4. Future Extensibility - New implementations without core changes

Why Immer for State?

Immer provides:

  1. Immutable Updates - Draft mutations become immutable updates
  2. RFC 6902 Patches - Standard format for change tracking
  3. Type Safety - Full TypeScript support
  4. Familiarity - Common pattern in React ecosystem

See Also

Released under the MIT License.