Skip to content

Core Concepts

Before building agents, let's understand the five core concepts in Helix Agents.

Agents

An agent is a configuration object that defines how an AI assistant behaves. It specifies:

  • What the agent knows (system prompt)
  • What it can do (tools)
  • What data it tracks (state schema)
  • What it produces (output schema)
  • How it thinks (LLM configuration)
typescript
import { defineAgent } from '@helix-agents/core';
import { z } from 'zod';

const ResearchAgent = defineAgent({
  name: 'researcher',
  systemPrompt: 'You are a research assistant. Search for information and summarize findings.',
  tools: [searchTool, summarizeTool],
  stateSchema: z.object({
    searchCount: z.number().default(0),
    findings: z.array(z.string()).default([]),
  }),
  outputSchema: z.object({
    summary: z.string(),
    sources: z.array(z.string()),
  }),
  llmConfig: {
    model: openai('gpt-4o'),
    temperature: 0.7,
  },
  maxSteps: 20,
});

An agent definition is just data - it doesn't execute anything. You pass it to a runtime to actually run.

Tools

Tools are functions that agents can call. They're how agents interact with the world beyond generating text.

typescript
import { defineTool } from '@helix-agents/core';
import { z } from 'zod';

const searchTool = defineTool({
  name: 'search',
  description: 'Search the web for information',
  inputSchema: z.object({
    query: z.string().describe('Search query'),
    maxResults: z.number().default(5),
  }),
  outputSchema: z.object({
    results: z.array(
      z.object({
        title: z.string(),
        url: z.string(),
        snippet: z.string(),
      })
    ),
  }),
  execute: async (input, context) => {
    // Perform the search
    const results = await performSearch(input.query, input.maxResults);
    return { results };
  },
});

Tools receive a context object that provides:

  • getState<T>() - Read current agent state
  • updateState<T>(draft => {...}) - Modify state using Immer
  • emit(eventName, data) - Emit custom streaming events
  • abortSignal - Check for cancellation
  • agentId, agentType - Execution context

State

State is data that persists across an agent's execution steps. There are two types:

Built-in State

Every agent automatically tracks:

  • messages - Conversation history
  • stepCount - Number of LLM calls made
  • status - Running, completed, failed, etc.
  • output - Final structured output (if any)

Custom State

You define additional state with a Zod schema:

typescript
const stateSchema = z.object({
  searchCount: z.number().default(0),
  findings: z.array(z.string()).default([]),
  currentTopic: z.string().optional(),
});

Tools can read and modify this state:

typescript
execute: async (input, context) => {
  // Read state
  const state = context.getState<typeof stateSchema>();
  console.log(`Search count: ${state.searchCount}`);

  // Update state using Immer's draft pattern
  context.updateState<typeof stateSchema>((draft) => {
    draft.searchCount++;
    draft.findings.push(input.query);
  });

  return { results };
};

State is persisted to the StateStore after each step, enabling resume after crashes.

Streaming

Streaming provides real-time visibility into agent execution. The framework emits typed events:

Event TypeDescription
text_deltaIncremental text from LLM
thinkingReasoning/thinking content (Claude, o-series)
tool_startTool execution beginning
tool_endTool execution complete (with result)
subagent_startSub-agent invocation beginning
subagent_endSub-agent complete (with output)
customCustom events from tools
state_patchState changes (RFC 6902 format)
errorError occurred
outputFinal agent output

Consume streams in your application:

typescript
const handle = await executor.execute(agent, 'Research AI agents');
const stream = await handle.stream();

for await (const chunk of stream) {
  switch (chunk.type) {
    case 'text_delta':
      process.stdout.write(chunk.delta);
      break;
    case 'tool_start':
      console.log(`\nCalling tool: ${chunk.toolName}`);
      break;
    case 'tool_end':
      console.log(`Tool result:`, chunk.result);
      break;
    case 'output':
      console.log('\nFinal output:', chunk.output);
      break;
  }
}

Sub-Agents

Sub-agents enable hierarchical agent systems. A parent agent can delegate tasks to specialized child agents.

typescript
// Define a specialized sub-agent
const AnalyzerAgent = defineAgent({
  name: 'analyzer',
  systemPrompt: 'You analyze text for sentiment and key topics.',
  outputSchema: z.object({
    sentiment: z.enum(['positive', 'negative', 'neutral']),
    topics: z.array(z.string()),
  }),
  llmConfig: { model: openai('gpt-4o-mini') },
});

// Create a tool that invokes the sub-agent
import { createSubAgentTool } from '@helix-agents/core';

const analyzeTool = createSubAgentTool(AnalyzerAgent, z.object({ text: z.string() }), {
  description: 'Analyze text for sentiment and topics',
});

// Parent agent uses the sub-agent tool
const ResearchAgent = defineAgent({
  name: 'researcher',
  tools: [searchTool, analyzeTool], // Include sub-agent tool
  // ...
});

When the parent's LLM calls subagent__analyzer, the framework:

  1. Creates a new agent run for the child
  2. Executes the child agent to completion
  3. Returns the child's output as the tool result
  4. Child events stream to the same stream as the parent

Sub-agents have isolated state - the child's state doesn't affect the parent's.

Putting It Together

Here's how these concepts work together in a typical execution:

1. Agent receives input message

2. LLM generates response (streaming text_delta events)

3. LLM requests tool calls

4. Tools execute (streaming tool_start/tool_end events)
   - Tools can read/modify state
   - Tools can emit custom events
   - Sub-agent tools spawn child executions

5. Tool results added to conversation

6. Loop back to step 2 until:
   - LLM calls __finish__ tool (structured output)
   - Max steps reached
   - Error occurs

7. Final output emitted

Next Steps

Released under the MIT License.