Production database dropped. During code freeze.
- Replit Agent
Fortune (July 2025), Tom's Hardware (July 2025)
- Replit Agent
The vocabulary of the tools.
The patterns they all share.
The ways they break.
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "What's 2+2?" }]
)
puts response.content.first.text
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "What's 2+2?" }]
)
puts response.content.first.text
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "What's 2+2?" }]
)
puts response.content.first.text
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: "What's 2+2?" }]
)
puts response.content.first.text
messages = []
loop do
print "> "
user_input = gets.chomp
messages << { role: "user", content: user_input }
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages
)
assistant_text = response.content.first.text
messages << { role: "assistant", content: assistant_text }
puts assistant_text
end
messages = []
loop do
print "> "
user_input = gets.chomp
messages << { role: "user", content: user_input }
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages
)
assistant_text = response.content.first.text
messages << { role: "assistant", content: assistant_text }
puts assistant_text
end
messages = []
loop do
print "> "
user_input = gets.chomp
messages << { role: "user", content: user_input }
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages
)
assistant_text = response.content.first.text
messages << { role: "assistant", content: assistant_text }
puts assistant_text
end
messages = []
loop do
print "> "
user_input = gets.chomp
messages << { role: "user", content: user_input }
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages
)
assistant_text = response.content.first.text
messages << { role: "assistant", content: assistant_text }
puts assistant_text
end
def get_weather(city:)
# Real impl would call a weather API
"It's 72°F and sunny in #{city}."
end
tools = [{
name: "get_weather",
description: "Get the current weather for a city.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}]
The model never sees our function. It only sees this schema.
tools = [{
name: "get_weather",
description: "Get the current weather for a city.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"]
}
}]
The model never sees our function. It only sees this schema.
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
# response.content might now contain a tool_use block:
# [#<Anthropic::ToolUseBlock
# type: "tool_use",
# name: "get_weather",
# input: { city: "Des Moines" }>]
The model can't run anything. It's just asking.
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
# response.content might now contain a tool_use block:
# [#<Anthropic::ToolUseBlock
# type: "tool_use",
# name: "get_weather",
# input: { city: "Des Moines" }>]
The model can't run anything. It's just asking.
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
# response.content might now contain a tool_use block:
# [#<Anthropic::ToolUseBlock
# type: "tool_use",
# name: "get_weather",
# input: { city: "Des Moines" }>]
The model can't run anything. It's just asking.
response.content.each do |block|
next unless block.type == "tool_use"
result =
case block.name
when "get_weather" then get_weather(**block.input)
# ... other tools
end
messages << {
role: "user",
content: [{
type: "tool_result",
tool_use_id: block.id,
content: result
}]
}
end
The harness dispatches. Every framework organizes this differently.
response.content.each do |block|
next unless block.type == "tool_use"
result =
case block.name
when "get_weather" then get_weather(**block.input)
# ... other tools
end
messages << {
role: "user",
content: [{
type: "tool_result",
tool_use_id: block.id,
content: result
}]
}
end
The harness dispatches. Every framework organizes this differently.
response.content.each do |block|
next unless block.type == "tool_use"
result =
case block.name
when "get_weather" then get_weather(**block.input)
# ... other tools
end
messages << {
role: "user",
content: [{
type: "tool_result",
tool_use_id: block.id,
content: result
}]
}
end
The harness dispatches. Every framework organizes this differently.
loop do
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
messages: messages,
tools: tools
)
messages << { role: "assistant", content: response.content }
tool_uses = response.content.select { |b| b.type == "tool_use" }
break if tool_uses.empty? # No tools called → agent is done
tool_uses.each do |block|
result = dispatch(block.name, block.input)
messages << {
role: "user",
content: [{
type: "tool_result",
tool_use_id: block.id,
content: result
}]
}
end
end
This is the agent. The loop drives itself. The break is the stop condition.
SYSTEM_PROMPT = <<~PROMPT
You are a helpful weather assistant. Always confirm
the city before checking weather. If the user asks about anything
other than weather, politely redirect them.
PROMPT
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: messages,
tools: tools
)
The system prompt is one of the biggest levers anyone has.
SYSTEM_PROMPT = <<~PROMPT
You are a helpful weather assistant. Always confirm
the city before checking weather. If the user asks about anything
other than weather, politely redirect them.
PROMPT
response = anthropic.messages.create(
model: :"claude-sonnet-4-5",
max_tokens: 1024,
system: SYSTEM_PROMPT,
messages: messages,
tools: tools
)
The system prompt is one of the biggest levers anyone has.
MAX_ITERATIONS = 25
iterations = 0
loop do
iterations += 1
raise "Agent exceeded #{MAX_ITERATIONS} iterations" if iterations > MAX_ITERATIONS
# ... rest of loop
end
Non-negotiable. Without this, a misbehaving model loops until your bill runs out.
~60 lines · 3 test cases · a real, working agent
A real agent.
The messages list grows forever. What happens when it hits the context window limit?
The messages list grows forever. What happens when it hits the context window limit?
"Context engineering" = a strategy for what to keep in the messages list.
Right now, if the model decides to call delete_file("/"), we just... do it.
Right now, if the model decides to call delete_file("/"), we just... do it.
Constraints on what the model can ask for AND what the harness will do.
The messages list dies when the process dies.
The messages list dies when the process dies.
A separate problem from in-conversation history.
What if the model invents a tool that doesn't exist? Or calls a real one with garbage arguments?
What if the model invents a tool that doesn't exist? Or calls a real one with garbage arguments?
agent.rb:45 — Fresh message list every iteration
def run(user_input)
# The summary carries all prior context, so the turn starts fresh.
@messages = [{ role: :user, content: user_input }]
agent.rb:134–143 — The extra summarization call
def compact!
response = @client.messages.create(
model: @model,
max_tokens: 512,
system: "#{SUMMARIZE_PROMPT}\nPrevious summary:\n#{@summary.empty? ? "(none yet)" : @summary}",
messages: @messages + [{ role: :user, content: "Provide the updated running summary now." }]
)
text = response.content.select { |block| block.type == :text }.map(&:text).join("\n")
@summary = text.strip
end
agent.rb:125–129 — Summary rides in the system prompt
def current_system
return @system_prompt if @summary.empty?
"#{@system_prompt}\n\nConversation so far:\n#{@summary}"
end
agent.rb:87 — The dangerous-tool check
if @registry.dangerous?(block.name) && !confirm?(block)
# A decline doesn't end the turn — the model still gets to react —
# but it's recorded as the exit_reason (the cap overrides it).
exit_reason = "dangerous_declined"
"The user declined to run #{block.name}."
else
@registry.dispatch(block.name, block.input)
end
agent.rb:161–166 — The human confirmation prompt
# Shows the human what the model wants to run and asks for a y/n.
def confirm?(block)
puts "Agent wants to run: #{block.name}(#{block.input.inspect})"
print "Allow? (y/n) "
gets&.chomp&.downcase == "y"
end
registry.rb:59–77 — Schema validation before dispatch
def validate(schema, input)
schema.fetch(:required, []).each do |field|
unless input.key?(field.to_sym)
return "Error: missing required field '#{field}'."
end
end
schema.fetch(:properties, {}).each do |field, spec|
next unless input.key?(field)
expected = JSON_TYPES.fetch(spec[:type], [Object])
unless expected.any? { |klass| input[field].is_a?(klass) }
return "Error: field '#{field}' should be a #{spec[:type]}, " \
"got #{input[field].inspect}."
end
end
nil
end
main.rb:34 — Save the summary on clean exit
# Save on clean exit only. A Ctrl-C mid-session won't reach here.
agent.save_memory
agent.rb:34 — Read the summary back on startup
# Resume from the last session's summary, if one was saved.
@summary = File.exist?(MEMORY_PATH) ? File.read(MEMORY_PATH) : ""
(Optionally pair with the save_memory definition at agent.rb:38–40:)
def save_memory
File.write(MEMORY_PATH, @summary)
end
registry.rb:42–47 — Recovery message instead of an exception
def dispatch(name, input)
tool = @tools[name]
# Recovery-oriented: name the valid tools so the model can self-correct.
unless tool
return "Error: no tool named '#{name}'. Available tools: #{@tools.keys.join(", ")}."
end
What you get: A library to build on
Loop: You declare the shape; the framework runs it
Tools: Vast pre-built library
Context: Pluggable
Models: Multi-Provider
Orchestration framework
When to use: Custom workflows. Heavy integrations. You'd rather assemble than build
What you get: A finished product
Loop: Hidden in the product
Tools: Fixed + MCP extensions
Context: Opinionated, aggressive
Models: Each locked to its provider
Agent as product
When to use: Coding work, today. Polished UX. You can live with vendor lock-in.
| Pi | OpenCode | |
|---|---|---|
| Form | Minimal library | Finished product |
| What you get | 5 readable packages | Installable CLI |
| Loop | Bottom-layer package | Open in the source |
| Tools | 4 core + extensions | Built-in suite + MCP + commands |
| Context | Manual | Multi-stage compaction |
| Models | Multi-provider | Multi-provider |
Six months from now there will be a framework I've never heard of.
You'll open the docs. And you'll see:
A loop
Tool definitions
A system prompt
Message history
Stop conditions
Context strategy
Guardrails