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:
- Interface-First Design - Core abstractions are interfaces, enabling multiple implementations
- Pure Functions for Logic - Orchestration logic is pure functions with no side effects
- Runtime Agnosticism - Agent definitions work across all runtimes unchanged
- 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 configtool.ts- Tool definition, sub-agent tools, finish toolstate.ts- Agent state, messages, thinking contentstream.ts- Stream chunks, all event typesruntime.ts- Step results, stop reasons, execution typesexecutor.ts- Executor interfacelogger.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, determineFinalStatusThese functions are used by all runtimes to ensure consistent behavior.
State (/state)
State tracking with Immer:
state/
└── immer-tracker.ts # ImmerStateTracker, RFC 6902 patchesLLM (/llm)
LLM adapter interface and utilities:
llm/
├── adapter.ts # LLMAdapter interface
├── mock-adapter.ts # MockLLMAdapter for testing
└── stop-reason.ts # Stop reason mapping utilitiesStore (/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 transportStream (/stream)
Stream utilities:
stream/
├── filters.ts # filterByType, excludeTypes, etc.
├── state-streamer.ts # CustomStateStreamer
├── state-projection.ts # StateProjection, StreamProjector
└── resumable-handler.ts # HTTP endpoint helpersData 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:
- Testability - Easy to unit test without mocking I/O
- Reusability - Same logic works across all runtimes
- Determinism - Required for Temporal workflow replay
- Clarity - Clear separation of logic and effects
Why Interfaces?
Store and adapter interfaces enable:
- Swappable Implementations - Dev vs production stores
- Testing - In-memory mocks for unit tests
- Platform Adaptation - Different stores for different platforms
- Future Extensibility - New implementations without core changes
Why Immer for State?
Immer provides:
- Immutable Updates - Draft mutations become immutable updates
- RFC 6902 Patches - Standard format for change tracking
- Type Safety - Full TypeScript support
- Familiarity - Common pattern in React ecosystem