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 stateprompt: The original prompt passed to the agentparams: Any parameters supplied via tool callslogger: A streaming logger that sends output to the UI viahandleStepsLogChunk
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 theend_turntool 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 setsgenerateN = nand 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:
-
agents/types/agent-definition.ts(lines 58-70): Defines theAgentDefinitioninterface including the optionalhandleStepsproperty with full TypeScript typing. -
packages/agent-runtime/src/run-programmatic-step.ts(lines 26-62): ContainsrunProgrammaticStep, the core executor that instantiates the generator, validates yields againstHandleStepsYieldValueSchema(lines 42-44), and routes each yield to the appropriate handler. -
sdk/src/agents/load-agents.ts(lines 240-245): Handles serialization ofhandleStepsfunctions to strings for storage and re-evaluation at runtime. This allows agent definitions to be saved and loaded while preserving their programmatic logic. -
sdk/src/run.ts: Wires uphandleStepsLogChunkto stream generator logs to the UI in real-time. -
packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts: Reference implementation showing howspawn_agentstool works, commonly invoked from withinhandleSteps.
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
handleStepsis an optional generator function in Codebuff that enables programmatic control over agent execution by yielding specific commands to the runtime.- Yield values include
ToolCallobjects for immediate tool execution,'STEP'for single LLM turns,'STEP_ALL'for autonomous operation, and special commands likeGENERATE_Nfor multiple responses. - Context access provides read-only
agentState, the originalprompt, toolparams, and a streamingloggerfor real-time UI feedback. - Source locations include
agents/types/agent-definition.ts(definition),packages/agent-runtime/src/run-programmatic-step.ts(execution), andsdk/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →