Core Concepts
Before building agents, let's understand the six 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)
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.
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 stateupdateState<T>(draft => {...})- Modify state using Immeremit(eventName, data)- Emit custom streaming eventsabortSignal- Check for cancellationagentId,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 historystepCount- Number of LLM calls madestatus- Running, completed, failed, etc.output- Final structured output (if any)
Custom State
You define additional state with a Zod schema:
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:
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 Type | Description |
|---|---|
text_delta | Incremental text from LLM |
thinking | Reasoning/thinking content (Claude, o-series) |
tool_start | Tool execution beginning |
tool_end | Tool execution complete (with result) |
subagent_start | Sub-agent invocation beginning |
subagent_end | Sub-agent complete (with output) |
custom | Custom events from tools |
state_patch | State changes (RFC 6902 format) |
error | Error occurred |
output | Final agent output |
Consume streams in your application:
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.
// 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:
- Creates a new agent run for the child
- Executes the child agent to completion
- Returns the child's output as the tool result
- 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.
Hooks
Hooks are callback functions that let you observe and react to agent execution events. Use them for logging, metrics, auditing, and tracing.
const agent = defineAgent({
name: 'researcher',
systemPrompt: 'You are a research assistant.',
hooks: {
onAgentStart: (payload, ctx) => {
console.log(`[${ctx.runId}] Starting with input: ${payload.input}`);
},
onAgentComplete: (payload, ctx) => {
console.log(`[${ctx.runId}] Completed in ${payload.durationMs}ms`);
},
beforeTool: (payload, ctx) => {
console.log(`[${ctx.runId}] Calling: ${payload.tool.name}`);
},
afterTool: (payload, ctx) => {
console.log(`[${ctx.runId}] ${payload.tool.name}: ${payload.success ? 'OK' : 'FAILED'}`);
},
},
// ...
});Available hooks include:
| Hook | When Invoked |
|---|---|
onAgentStart | Before first LLM call |
onAgentComplete | Agent finished successfully |
onAgentFail | Agent failed with error |
beforeLLMCall | Before each LLM call |
afterLLMCall | After each LLM response |
beforeTool / afterTool | Around tool execution |
beforeSubAgent / afterSubAgent | Around sub-agent execution |
onStateChange | When state is modified |
onMessage | When message is added |
Hooks receive a context object similar to tools, with access to state and streaming capabilities.
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 emittedNext Steps
- Getting Started - Build your first agent
- Defining Agents - Deep dive into agent configuration
- Defining Tools - Complete tool reference
- Hooks - Observability and callbacks