Skip to content

Getting Started

Let's build your first AI agent with Helix Agents. We'll create a simple research assistant that can search the web and summarize findings.

Prerequisites

  • Node.js 22+ and npm 11+
  • An OpenAI API key (or another LLM provider)

Installation

Install the SDK (which bundles core, memory stores, and JS runtime) plus the LLM adapter:

bash
npm install @helix-agents/sdk @helix-agents/llm-vercel @ai-sdk/openai zod

Understanding What We're Building

Before writing code, let's understand the pieces:

  1. Output Schema - What the agent produces (a research summary)
  2. Tools - Actions the agent can take (searching, taking notes)
  3. Agent Definition - Configuration combining everything
  4. Executor - The runtime that executes the agent
  5. Execution - Starting the agent and streaming results

Step 1: Define the Output Schema

Our research assistant will produce a structured summary. Define this with Zod:

typescript
import { z } from 'zod';

// What the agent will return when finished
const ResearchOutputSchema = z.object({
  topic: z.string().describe('The topic that was researched'),
  summary: z.string().describe('A comprehensive summary of findings'),
  keyPoints: z.array(z.string()).describe('Key takeaways'),
  sources: z.array(z.string()).describe('URLs of sources used'),
});

type ResearchOutput = z.infer<typeof ResearchOutputSchema>;

The outputSchema tells the framework to inject a special __finish__ tool. When the LLM calls this tool with valid data, the agent completes.

Step 2: Define a Tool

Tools give agents capabilities. Let's create a search tool:

typescript
import { defineTool } from '@helix-agents/sdk';

const searchTool = defineTool({
  name: 'search',
  description: 'Search the web for information on a topic',

  // What the LLM provides when calling this tool
  inputSchema: z.object({
    query: z.string().describe('The search query'),
  }),

  // What the tool returns
  outputSchema: z.object({
    results: z.array(
      z.object({
        title: z.string(),
        url: z.string(),
        snippet: z.string(),
      })
    ),
  }),

  // The actual implementation
  execute: async ({ query }, context) => {
    // In a real app, you'd call a search API
    // For now, we'll return mock results
    console.log(`Searching for: ${query}`);

    return {
      results: [
        {
          title: `${query} - Wikipedia`,
          url: `https://en.wikipedia.org/wiki/${encodeURIComponent(query)}`,
          snippet: `Learn about ${query} and its various aspects...`,
        },
        {
          title: `Understanding ${query}`,
          url: `https://example.com/${encodeURIComponent(query)}`,
          snippet: `A comprehensive guide to ${query}...`,
        },
      ],
    };
  },
});

The context parameter provides access to state, custom events, and more. We'll explore this in the Tools guide.

Step 3: Define the Agent

Now combine everything into an agent definition:

typescript
import { defineAgent } from '@helix-agents/sdk';
import { openai } from '@ai-sdk/openai';

const ResearchAgent = defineAgent({
  // Unique identifier for this agent type
  name: 'research-assistant',

  // Instructions for the LLM
  systemPrompt: `You are a helpful research assistant. When given a topic:
1. Search for relevant information using the search tool
2. Analyze the results carefully
3. Provide a comprehensive summary with key points
4. Always cite your sources

Be thorough but concise. Focus on factual, well-sourced information.`,

  // Tools the agent can use
  tools: [searchTool],

  // The structured output schema
  outputSchema: ResearchOutputSchema,

  // LLM configuration
  llmConfig: {
    model: openai('gpt-4o-mini'),
    temperature: 0.7,
  },

  // Safety limit: maximum LLM calls before stopping
  maxSteps: 10,
});

Agent Definition is Just Data

defineAgent() doesn't execute anything - it returns a configuration object. This is intentional: you can define agents without any runtime, then execute them with different runtimes in different environments.

Step 4: Create the Executor

The executor runs agents. For development, use the JS runtime with in-memory stores:

typescript
import { JSAgentExecutor, InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/sdk';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';

// State store: where agent state persists between steps
const stateStore = new InMemoryStateStore();

// Stream manager: handles real-time event streaming
const streamManager = new InMemoryStreamManager();

// LLM adapter: bridges to the Vercel AI SDK
const llmAdapter = new VercelAIAdapter();

// Create the executor
const executor = new JSAgentExecutor(stateStore, streamManager, llmAdapter);

Swappable Components

In production, you'd swap InMemoryStateStore for RedisStateStore and potentially use a different runtime like Temporal. The agent definition stays the same.

Step 5: Execute and Stream Results

Now let's run the agent:

typescript
async function main() {
  // Make sure you have OPENAI_API_KEY set
  if (!process.env.OPENAI_API_KEY) {
    console.error('Please set OPENAI_API_KEY environment variable');
    process.exit(1);
  }

  console.log('Starting research on AI agents...\n');

  // Start execution
  const handle = await executor.execute(
    ResearchAgent,
    'Research the benefits and challenges of AI agents in software development'
  );

  // Get the stream
  const stream = await handle.stream();

  if (stream) {
    // Process streaming events
    for await (const chunk of stream) {
      switch (chunk.type) {
        case 'text_delta':
          // Incremental text from the LLM
          process.stdout.write(chunk.delta);
          break;

        case 'tool_start':
          // Tool is about to execute
          console.log(`\n[Tool: ${chunk.toolName}]`);
          break;

        case 'tool_end':
          // Tool finished
          console.log(`[Result: ${JSON.stringify(chunk.result).slice(0, 100)}...]`);
          break;

        case 'error':
          // Something went wrong
          console.error(`\n[Error: ${chunk.error}]`);
          break;
      }
    }
  }

  // Get the final result
  const result = await handle.result();

  console.log('\n\n=== Research Complete ===');
  console.log('Status:', result.status);

  if (result.output) {
    console.log('\nTopic:', result.output.topic);
    console.log('\nSummary:', result.output.summary);
    console.log('\nKey Points:');
    result.output.keyPoints.forEach((point, i) => {
      console.log(`  ${i + 1}. ${point}`);
    });
    console.log('\nSources:', result.output.sources.join(', '));
  }
}

main().catch(console.error);

Complete Example

Here's the full code in one file:

typescript
// research-agent.ts
import { defineAgent, defineTool } from '@helix-agents/sdk';
import { JSAgentExecutor, InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/sdk';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';

// Output schema
const ResearchOutputSchema = z.object({
  topic: z.string(),
  summary: z.string(),
  keyPoints: z.array(z.string()),
  sources: z.array(z.string()),
});

// Search tool
const searchTool = defineTool({
  name: 'search',
  description: 'Search the web for information',
  inputSchema: z.object({ query: z.string() }),
  outputSchema: z.object({
    results: z.array(
      z.object({
        title: z.string(),
        url: z.string(),
        snippet: z.string(),
      })
    ),
  }),
  execute: async ({ query }) => ({
    results: [
      {
        title: `${query} - Wikipedia`,
        url: `https://wikipedia.org`,
        snippet: `Info about ${query}`,
      },
    ],
  }),
});

// Agent definition
const ResearchAgent = defineAgent({
  name: 'research-assistant',
  systemPrompt: 'You are a research assistant. Search for info and provide summaries.',
  tools: [searchTool],
  outputSchema: ResearchOutputSchema,
  llmConfig: { model: openai('gpt-4o-mini') },
  maxSteps: 10,
});

// Execute
async function main() {
  const executor = new JSAgentExecutor(
    new InMemoryStateStore(),
    new InMemoryStreamManager(),
    new VercelAIAdapter()
  );

  const handle = await executor.execute(ResearchAgent, 'Benefits of TypeScript');

  for await (const chunk of (await handle.stream()) ?? []) {
    if (chunk.type === 'text_delta') process.stdout.write(chunk.delta);
  }

  const result = await handle.result();
  console.log('\n\nOutput:', JSON.stringify(result.output, null, 2));
}

main();

Run it:

bash
OPENAI_API_KEY=sk-your-key npx tsx research-agent.ts

Understanding What Happened

Let's trace through the execution:

  1. Initialization: The executor created a new agent run with a unique runId and streamId

  2. First LLM Call: The agent's system prompt plus the user message were sent to GPT-4o-mini

  3. Tool Call: The LLM decided to use the search tool, returning {"name": "search", "arguments": {"query": "..."}}

  4. Tool Execution: The search tool ran, and its result was added to the conversation

  5. Second LLM Call: The LLM saw the search results and decided what to do next

  6. Finish: Eventually, the LLM called the __finish__ tool with structured output matching our schema

  7. Result: The agent completed with status completed and typed output

Next Steps

You now have a working agent! Here's where to go next:

Released under the MIT License.