ReAct lets a model choose among tools you wrote. CodeAct lets the model write the Ruby operation it wants to execute, inspect the result, and continue until it has an answer.
That flexibility changes the safety boundary. The current dspy-code_act implementation calls Ruby eval in the application process. The example below is suitable for controlled experiments with trusted input. It is not a sandbox for production or multi-tenant workloads.
Define the task
The signature describes the research input and final output:
class ResearchQuery < DSPy::Signature
description "Research a topic by generating Ruby code to fetch and analyze web content."
input do
const :query, String, description: "The research question or topic to investigate"
const :context, String, description: "Available libraries and execution constraints"
end
output do
const :answer, String, description: "The answer supported by execution results"
end
end
The signature fixes the task boundary. CodeAct still lets the model choose the code and whether another iteration is needed.
Describe the execution context
The context field tells the model which libraries and endpoints are relevant:
RESEARCH_CONTEXT = <<~CONTEXT
You have access to these Ruby libraries:
- Net::HTTP for HTTP requests
- URI for parsing URLs
- JSON for parsing JSON responses
Useful APIs:
- GitHub API: https://api.github.com/repos/{owner}/{repo}
- Wikipedia API: https://en.wikipedia.org/api/rest_v1/page/summary/{title}
CONTEXT
This text guides code generation. It does not restrict what eval can access. A real execution policy must be enforced outside the generated program.
Run the agent
Install the optional gem and configure an LM:
gem 'dspy'
gem 'dspy-code_act'
DSPy.configure do |config|
config.lm = DSPy::LM.new(
'openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY']
)
end
agent = DSPy::CodeAct.new(ResearchQuery, max_iterations: 8)
result = agent.call(
query: "How many stars does rails/rails have?",
context: RESEARCH_CONTEXT
)
puts result.answer
CodeAct runs a Think-Code-Observe loop:
- A predictor returns a thought and Ruby code.
- CodeAct executes the code and captures stdout or the evaluated result.
- A second predictor inspects the observation and chooses
continueorfinish. - The loop stops at
finishormax_iterations.
The model owns the bounded action choice. The application owns the iteration limit, execution environment, network policy, credentials, and termination behavior around the loop.
Inspect a run
A recorded run generated code equivalent to:
uri = URI.parse('https://api.github.com/repos/rails/rails')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
response = http.get(uri.request_uri)
data = JSON.parse(response.body)
puts "rails/rails has #{data['stargazers_count']} stars."
The result includes typed history entries:
result.history.each do |entry|
puts "Step #{entry.step}"
puts entry.thought
puts entry.ruby_code
puts entry.execution_result
warn entry.error_message unless entry.error_message.empty?
end
The star count in an old trace will become stale. The useful evidence is the selected endpoint, generated code, captured observation, and decision to finish.
Signatures specialize the loop
Changing the signature changes the task and result contract without changing CodeAct’s execution loop:
class AnalyzeCSV < DSPy::Signature
description "Analyze CSV data and compute requested statistics."
input do
const :csv_data, String
const :question, String
end
output do
const :analysis, String
const :numbers, T::Array[Float]
end
end
The model still receives natural-language task and field descriptions. DSPy.rb turns those descriptions and the execution history into provider prompts; you do not maintain the multi-step prompt template yourself.
Choose CodeAct or ReAct
Use CodeAct when the useful operation cannot be enumerated cleanly in advance, such as exploratory transformations over trusted data. Use ReAct when the allowed operations are known and each tool can expose a narrow typed interface.
| Property | ReAct | CodeAct |
|---|---|---|
| Model selects | A declared tool and typed arguments | Ruby code to execute |
| Authority boundary | Tool implementations | Execution environment |
| Best fit | Known APIs and controlled side effects | Open-ended computation in isolation |
| Primary risk | Overpowered tools | Arbitrary code execution |
Prefer ReAct when either approach can solve the task. A smaller authority surface is easier to test.
Isolation is part of the harness
Timeouts and forbidden-string checks do not make eval safe. Generated Ruby can reach constants, loaded libraries, the filesystem, environment variables, network clients, and process APIs through many equivalent expressions.
For untrusted input, execute CodeAct in a disposable boundary with:
- A separate process or container.
- No inherited application secrets.
- An allowlisted network policy.
- Read-only or ephemeral storage.
- CPU, memory, wall-clock, and output limits.
- A narrow protocol for returning observations.
Ruby’s $SAFE mechanism was removed in Ruby 3.0. Container or virtual-machine isolation adds overhead, but that overhead is part of running generated code honestly.
Try the example
The full script is examples/codeact_research_agent.rb:
bundle exec ruby examples/codeact_research_agent.rb \
"How many stars does rails/rails have?"
Run it interactively by omitting the question:
bundle exec ruby examples/codeact_research_agent.rb
Keep the demo local and controlled. Before connecting CodeAct to user input, move execution out of the application process and test the boundary as seriously as the agent loop.