handleSteps Generator Function in Codebuff: A Complete Guide to Programmatic Agent Control

The handleSteps generator function in Codebuff lets you script agent execution by yielding tool calls, step commands, and special directives, giving you precise control over when the LLM runs and when tools are invoked.

The handleSteps generator function is an optional but powerful feature in the Codebuff repository that transforms how agents execute tasks. By adding this function to your agent definition, you replace the default LLM-driven loop with a programmatic script that yields specific instructions to the runtime. This approach is implemented in packages/agent-runtime/src/run-programmatic-step.ts and defined in agents/types/agent-definition.ts, giving developers fine-grained control over tool execution, LLM stepping, and output generation.

What is the handleSteps Generator Function?

handleSteps is a generator function (using the function* syntax) that you attach to an AgentDefinition object. When Codebuff spawns your agent, the programmatic step executor (runProgrammaticStep) creates a generator instance from this function and drives it step-by-step. Each time the generator yields a value, the runtime interprets that value as a command, executes it, and sends the result back to the generator.

The generator receives a context object containing:

  • agentState: Read-only access to the current agent state
  • prompt: The original prompt passed to the agent
  • params: Any parameters supplied via tool calls
  • logger: A streaming logger that sends output to the UI via handleStepsLogChunk

This context allows you to inspect the current execution state and emit logs that appear in real-time in the Codebuff interface.

Yield Values and Execution Control

The runtime validates every yielded value against HandleStepsYieldValueSchema (defined at line 42-44 in run-programmatic-step.ts). Invalid yields trigger immediate errors with clear messages. Here are the valid yield patterns:

Tool Calls

Yield a ToolCall object to execute any built-in tool immediately:

const { toolResult } = yield {
  toolName: 'read_files',
  input: { paths: ['src/index.ts'] }
}

Available tools include read_files, spawn_agents, set_output, and any custom tools defined in your project.

Step Control (STEP and STEP_ALL)

Control when the LLM generates responses:

  • 'STEP': Runs the agent's LLM once, producing a single assistant message. Use this to get the LLM's input before deciding the next tool call.
  • 'STEP_ALL': Enters continuous mode where the LLM keeps generating messages until it calls the end_turn tool or stops producing tool calls. This is useful for letting the LLM work autonomously through a complex task.
// Single step
yield 'STEP'

// Continuous execution
yield 'STEP_ALL'

Text Parsing and Generation

  • { type: 'STEP_TEXT', text }: Parses the provided text for embedded tool calls, then executes those calls in order. This allows dynamic tool invocation based on generated content.
  • { type: 'GENERATE_N', n }: Requests n separate LLM responses. The runtime sets generateN = n and returns multiple outputs, useful for "best-of-N" scenarios where you want to pick the highest quality response.

Ending Execution

When the generator returns or finishes, the current agent's turn ends automatically. You can also explicitly return to signal completion.

Implementation Details and Source Locations

Understanding where handleSteps is defined and executed helps when debugging or extending functionality:

Practical Examples

Basic File Reader

This agent reads a file and returns its contents using programmatic control:

import type { AgentDefinition } from './types/agent-definition'

export const definition: AgentDefinition = {
  id: 'file-reader',
  displayName: 'File Reader',
  model: 'anthropic/claude-sonnet-4.6',
  instructionsPrompt: 'Read the requested file and output its contents.',
  handleSteps: function* ({ agentState, prompt, logger }) {
    logger.info('Starting file read operation')
    
    // Get LLM acknowledgment (optional)
    yield 'STEP'
    
    // Execute read_files tool
    const { toolResult } = yield {
      toolName: 'read_files',
      input: { paths: [prompt ?? 'README.md'] },
    }
    
    // Return result via set_output
    yield {
      toolName: 'set_output',
      input: { 
        output: toolResult?.[0]?.content ?? 'No content found' 
      },
    }
    
    return
  },
}

export default definition

Continuous Execution with STEP_ALL

Use this pattern when you want the LLM to work autonomously until it decides to stop:

handleSteps: function* ({ logger }) {
  logger.info('Entering autonomous mode')
  
  // LLM keeps generating until it calls end_turn
  yield 'STEP_ALL'
  
  logger.info('Agent finished autonomous execution')
}

Spawning Sub-agents

Orchestrate complex workflows by spawning specialized sub-agents:

handleSteps: function* ({ logger, prompt }) {
  logger.debug('Delegating to specialist sub-agent')
  
  // Spawn a "thinker" agent to analyze the prompt
  const { toolResult } = yield {
    toolName: 'spawn_agents',
    input: {
      agents: [
        {
          agent_type: 'thinker',
          prompt: `Analyze this request deeply: ${prompt}`,
        },
      ],
    },
  }
  
  // Wait for sub-agent completion with a single step
  const { stepsComplete } = yield 'STEP'
  
  if (stepsComplete && toolResult?.[0]) {
    yield {
      toolName: 'set_output',
      input: { output: toolResult[0].content },
    }
  }
}

Best-of-N Generation

Generate multiple responses and select the best one:

handleSteps: function* ({ logger }) {
  logger.info('Generating 3 candidate responses')
  
  // Request 3 separate LLM generations
  yield { type: 'GENERATE_N', n: 3 }
  
  // The runtime will provide all 3 responses for comparison
  yield 'STEP'
  
  // Logic to select best response would go here...
}

Summary

  • handleSteps is an optional generator function in Codebuff that enables programmatic control over agent execution by yielding specific commands to the runtime.
  • Yield values include ToolCall objects for immediate tool execution, 'STEP' for single LLM turns, 'STEP_ALL' for autonomous operation, and special commands like GENERATE_N for multiple responses.
  • Context access provides read-only agentState, the original prompt, tool params, and a streaming logger for real-time UI feedback.
  • Source locations include agents/types/agent-definition.ts (definition), packages/agent-runtime/src/run-programmatic-step.ts (execution), and sdk/src/agents/load-agents.ts (serialization).
  • Validation ensures every yield matches HandleStepsYieldValueSchema, throwing clear errors for invalid values before execution proceeds.

Frequently Asked Questions

How do I add handleSteps to my existing agent definition?

Add a handleSteps property to your AgentDefinition object in your agent file (typically located in a .agents/ folder). The value must be a generator function (function*) that accepts a context object. According to the source code in agents/types/agent-definition.ts (lines 58-70), this property is optional and fully typed, so TypeScript will validate your generator's signature automatically.

What happens if I yield an invalid value from handleSteps?

The runtime validates every yielded value against HandleStepsYieldValueSchema defined in packages/agent-runtime/src/run-programmatic-step.ts (lines 42-44). If you yield an unrecognized object or malformed structure, the executor throws a validation error immediately and halts the agent's execution. This strict typing prevents silent failures and ensures that only supported operations (ToolCalls, STEP commands, or special directives) reach the execution engine.

Can I use async operations inside handleSteps?

No, handleSteps must be a synchronous generator function (function*, not async function*). The runtime in runProgrammaticStep expects to drive the generator step-by-step using .next(), and async generators would complicate the execution model. However, you can yield ToolCalls that perform asynchronous operations (like read_files or spawn_agents), and the runtime will pause your generator until the tool completes, effectively giving you async capabilities through the yield mechanism.

How does logging work inside the handleSteps generator?

The generator receives a logger object in its context parameter that streams logs directly to the Codebuff UI. When you call logger.info(), logger.debug(), or similar methods, the runtime in sdk/src/run.ts routes these through handleStepsLogChunk to provide real-time feedback during execution. This is particularly useful for debugging complex generator flows or showing progress when spawning sub-agents or running multiple steps, as the logs appear in the UI without waiting for the generator to complete.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →