# How to Build a Programmatic Agent Using `handleSteps` Generators in FreeBuff

> Learn to build programmatic agents with FreeBuff handleSteps generators. Execute step-by-step workflows efficiently using this powerful feature.

- Repository: [Codebuff/freebuff](https://github.com/CodebuffAI/freebuff)
- Tags: how-to-guide
- Published: 2026-08-20

---

**FreeBuff agents use generator functions in the `handleSteps` property to execute step-by-step workflows, yielding tool calls and log chunks that the runtime processes sequentially until completion or step limit.**

The FreeBuff SDK enables fully programmable agents through a template-based system. An agent definition exports a **`handleSteps`** generator that the SDK serializes, transmits to the runtime, and executes step-by-step. Each yielded value becomes an instruction—whether a tool invocation, progress log, or control signal—that drives the agent's behavior programmatically.

## Understanding the `handleSteps` Execution Flow

Building a programmatic agent using `handleSteps` generators involves seven distinct phases, each handled by specific components in the FreeBuff source code.

### Phase 1: Define the Agent Template

The agent creator exports an `AgentDefinition` object containing the `handleSteps` generator function. This definition resides in the agents package and establishes the agent's identity and behavior.

```typescript
// agents/types/agent-definition.ts
export interface AgentDefinition {
  name: string;
  description: string;
  handleSteps?: () => Generator<HandleStepsYieldValue, any, any>;
}

```

### Phase 2: Serialize for Transport

When agents are loaded, the SDK converts the generator function to a string for serialization while preserving a live reference for local execution. In [`sdk/src/agents/load-agents.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/agents/load-agents.ts), the loader performs:

```typescript
agentDefinition.handleSteps = fn.toString();
const handleStepsFn = fn; // Live reference preserved

```

This dual representation enables both remote runtime execution and local development workflows.

### Phase 3: Initialize the Session

The client creates a run session with step-limit protection. In [`sdk/src/client.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/client.ts), the `run` method seeds `stepsRemaining` from `maxAgentSteps` (defaulting to 200):

```typescript
const runOptions = {
  maxAgentSteps: options.maxAgentSteps ?? 200,
  // ... session configuration
};

```

### Phase 4: Execute Programmatic Steps

The runtime deserializes and drives the generator. The core driver in [`packages/agent-runtime/src/run-programmatic-step.ts`](https://github.com/CodebuffAI/freebuff/blob/main/packages/agent-runtime/src/run-programmatic-step.ts) uses `globalEval` to reconstruct the generator function, then repeatedly invokes `next()` until exhaustion or step limit:

```typescript
const generator = globalEval(agentDefinition.handleSteps)();
let result = generator.next();

while (!result.done && stepsRemaining > 0) {
  const yieldedValue = result.value;
  // Process yield...
  result = generator.next(toolResult);
  stepsRemaining--;
}

```

### Phase 5: Validate and Route Yields

Each yielded value passes through `HandleStepsYieldValueSchema` validation. Tool requests route to the Tool Executor; results feed back into the generator via `next()`:

```typescript
// packages/agent-runtime/src/util/parse-tool-calls-from-text.ts
const validated = HandleStepsYieldValueSchema.parse(yieldedValue);
if (validated.tool) {
  const toolResult = await toolExecutor.execute(validated.tool, validated.args);
  result = generator.next(toolResult);
}

```

### Phase 6: Stream Progress Logs

When the generator yields `handleStepsLogChunk`, the runtime forwards it to the client's callback for UI rendering:

```typescript
yield { handleStepsLogChunk: 'Processing file...' };
// Runtime delivers to: options.handleStepsLogChunk?.(msg)

```

### Phase 7: Return Final Output

Generator completion propagates the return value as the agent's final result, terminating the run session.

## Creating a `handleSteps` Generator Agent

Follow this implementation pattern to build your own programmatic agent using `handleSteps` generators.

### Step 1: Structure Your Generator Function

The `handleSteps` property must be a **generator function** (`function*`) yielding objects matching the `HandleStepsYieldValueSchema`. Valid yield types include:

- **Tool calls**: `{ tool: string, args: object }`
- **Log chunks**: `{ handleStepsLogChunk: string }`
- **Error signals**: `{ error: string }`

```typescript
function* workflowGenerator() {
  // Emit progress visible to users
  yield { handleStepsLogChunk: 'Initializing analysis...' };
  
  // Execute a tool and capture the result
  const fileContent = yield {
    tool: 'readFile',
    args: { path: 'src/config.ts' }
  };
  
  // Use the result in subsequent logic
  yield { handleStepsLogChunk: `Read ${fileContent.length} bytes` };
  
  // Return final output
  return { success: true, data: fileContent };
}

```

### Step 2: Export the Complete Definition

Create your agent file in the `agents/` directory with full type safety:

```typescript
// agents/src/my-analyzer.ts
import type { AgentDefinition } from '@codebuff/common/types/agent-template';

export const fileAnalyzer: AgentDefinition = {
  name: 'file-analyzer',
  description: 'Reads and analyzes source files programmatically',
  
  handleSteps: function* () {
    // Log: operation start
    yield { handleStepsLogChunk: '🔍 Starting file analysis' };
    
    // Tool: read package.json for context
    const packageJson = yield {
      tool: 'readFile',
      args: { path: 'package.json' }
    };
    
    // Log: intermediate progress
    const pkg = JSON.parse(packageJson);
    yield {
      handleStepsLogChunk: `📦 Project: ${pkg.name}@${pkg.version}`
    };
    
    // Tool: list source files
    const sourceFiles = yield {
      tool: 'listDirectory',
      args: { path: 'src', recursive: false }
    };
    
    // Log: completion status
    yield {
      handleStepsLogChunk: `✅ Found ${sourceFiles.length} files in src/`
    };
    
    // Return structured result
    return {
      project: pkg.name,
      filesAnalyzed: sourceFiles.length,
      fileList: sourceFiles
    };
  }
};

```

### Step 3: Register and Export

Ensure your agent is discoverable by adding it to the package index:

```typescript
// agents/src/index.ts
export { fileAnalyzer } from './my-analyzer';

```

### Step 4: Execute with the SDK Client

Invoke your agent through the runtime using the `client.run` API:

```typescript
// examples/run-analyzer.ts
import { client } from '@codebuff/sdk';
import { fileAnalyzer } from '@codebuff/agents';

async function analyzeProject() {
  const result = await client.run({
    agents: [fileAnalyzer],
    maxAgentSteps: 15,                    // Prevent runaway execution
    handleStepsLogChunk: (msg: string) => {
      console.log(`[${new Date().toISOString()}] ${msg}`);
    }
  });
  
  console.log('Analysis complete:', result);
  return result;
}

analyzeProject().catch(console.error);

```

## Working with Tool Results and State

The `handleSteps` generator maintains state across yields. Tool results passed through `generator.next()` become available for subsequent logic:

```typescript
handleSteps: function* () {
  // First tool call
  const userQuery = yield { tool: 'getUserInput', args: {} };
  
  // Use result in second tool call
  const searchResults = yield {
    tool: 'webSearch',
    args: { query: userQuery }
  };
  
  // Chain results into final processing
  const summary = yield {
    tool: 'summarize',
    args: { text: searchResults.join('\n') }
  };
  
  return { answer: summary };
}

```

This synchronous-looking code executes asynchronously, with the runtime managing all promise resolution and error handling between steps.

## Advanced `handleSteps` Patterns

### Nested Agent Delegation

Spawn sub-agents within a generator using `loopAgentSteps`, as implemented in the built-in researcher agents:

```typescript
handleSteps: function* () {
  const researchTask = yield {
    tool: 'spawnAgent',
    args: {
      agentName: 'deep-researcher',
      inputs: { topic: 'quantum computing' }
    }
  };
  
  // Process sub-agent output
  yield {
    handleStepsLogChunk: `Research produced ${researchTask.pages} pages`
  };
  
  return { synthesized: researchTask.findings };
}

```

Note that `spawnAgent` sets `fromHandleSteps: false` internally to prevent infinite recursion cycles.

### Error Recovery Strategies

Tool failures yield error objects that generators can handle gracefully:

```typescript
handleSteps: function* () {
  yield { handleStepsLogChunk: 'Attempting file read' };
  
  const result = yield {
    tool: 'readFile',
    args: { path: 'maybe-missing.txt' }
  };
  
  // Check for error structure
  if (result && typeof result === 'object' && 'error' in result) {
    yield { handleStepsLogChunk: `Fallback: ${result.error}` };
    
    // Attempt recovery
    const fallback = yield {
      tool: 'readFile',
      args: { path: 'backup.txt' }
    };
    
    return { data: fallback, source: 'backup' };
  }
  
  return { data: result, source: 'primary' };
}

```

### Step Budget Management

Monitor remaining steps for long-running workflows:

```typescript
handleSteps: function* () {
  const maxIterations = 5;
  
  for (let i = 0; i < maxIterations; i++) {
    yield { handleStepsLogChunk: `Iteration ${i + 1}/${maxIterations}` };
    
    const progress = yield { tool: 'processBatch', args: { batchId: i } };
    
    if (progress.complete) {
      yield { handleStepsLogChunk: 'Early termination: goal achieved' };
      break;
    }
  }
  
  return { iterations: i + 1 };
}

```

The runtime enforces the global `maxAgentSteps` ceiling regardless of loop structures.

## Key Source Files Reference

| File | Purpose |
|------|---------|
| [`packages/agent-runtime/src/run-programmatic-step.ts`](https://github.com/CodebuffAI/freebuff/blob/main/packages/agent-runtime/src/run-programmatic-step.ts) | Core driver: deserializes generators, iterates yields, manages step counting |
| [`sdk/src/agents/load-agents.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/agents/load-agents.ts) | Serializes `handleSteps` functions for runtime transport |
| [`sdk/src/client.ts`](https://github.com/CodebuffAI/freebuff/blob/main/sdk/src/client.ts) | Public API: `client.run` with `maxAgentSteps` and logging options |
| [`agents/types/agent-definition.ts`](https://github.com/CodebuffAI/freebuff/blob/main/agents/types/agent-definition.ts) | TypeScript interfaces for `AgentDefinition` and yield schemas |
| [`packages/agent-runtime/src/tools/tool-executor.ts`](https://github.com/CodebuffAI/freebuff/blob/main/packages/agent-runtime/src/tools/tool-executor.ts) | Executes tool calls yielded from generators |
| [`packages/agent-runtime/src/util/parse-tool-calls-from-text.ts`](https://github.com/CodebuffAI/freebuff/blob/main/packages/agent-runtime/src/util/parse-tool-calls-from-text.ts) | Validates and parses `HandleStepsYieldValueSchema` |

## Summary

- **FreeBuff programmatic agents** center on the `handleSteps` generator pattern defined in `AgentDefinition`
- **Generator functions** (`function*`) yield tool calls, log chunks, and control signals that the runtime processes sequentially
- **Serialization** in [`load-agents.ts`](https://github.com/CodebuffAI/freebuff/blob/main/load-agents.ts) enables both local development and remote runtime execution
- **Execution** in [`run-programmatic-step.ts`](https://github.com/CodebuffAI/freebuff/blob/main/run-programmatic-step.ts) handles deserialization, validation, tool execution, and step limiting
- **State management** flows through `generator.next()` calls, with tool results feeding back into subsequent yields
- **Safety defaults** include 200-step limits via `maxAgentSteps` and schema validation on all yields

## Frequently Asked Questions

### What yield values are valid in a `handleSteps` generator?

Valid yields must conform to `HandleStepsYieldValueSchema`. Common patterns include `{ tool: 'name', args: {} }` for tool execution, `{ handleStepsLogChunk: 'message' }` for progress streaming, and `{ error: 'description' }` for error signaling. The runtime validates each yield and routes it appropriately.

### How does the runtime handle generator state across async tool calls?

The runtime maintains the generator instance in [`run-programmatic-step.ts`](https://github.com/CodebuffAI/freebuff/blob/main/run-programmatic-step.ts), calling `next()` with resolved tool results. This creates the illusion of synchronous execution while the runtime manages underlying promises. Generator-local variables persist across yields, enabling stateful workflows.

### Can I use `handleSteps` with TypeScript async generators?

No—`handleSteps` specifically expects synchronous generators (`function*`, not `async function*`). The asynchronous handling occurs in the runtime layer, not within the generator itself. Attempting to use `async function*` will fail serialization in [`load-agents.ts`](https://github.com/CodebuffAI/freebuff/blob/main/load-agents.ts) and break the execution model.

### What happens when `maxAgentSteps` is exceeded?

The runtime forcibly terminates generator execution. In [`run-programmatic-step.ts`](https://github.com/CodebuffAI/freebuff/blob/main/run-programmatic-step.ts), the loop condition checks `stepsRemaining > 0` before each `next()` call. Upon exhaustion, the run ends with an incomplete status and partial results. Always set conservative limits and monitor `handleStepsLogChunk` for progress visibility.