Self-Evolving Apps: Not Vibe, Live-Coding in Ruby with LLMs and Metaprogramming
Using AI to generate code for a new application is a familiar workflow today. But what if an application starts as a completely blank slate, learning on the job and writing its own implementation live as you call nonexistent methods? This concept of live-patching and zero-downtime execution isn't entirely new. Early in my career at Nortel Networks, I have seen this with PROTEL (PRocess Oriented TELepony language), a proprietary language designed for telecom switches. To achieve "five nines" (99.999%) availability, you couldn't simply take systems offline for deployments-code updates had to happen via live hot-patching. Later on, I thoroughly enjoyed similar live code reloading capabilities while working with Erlang. Recently, as I spent more time with Ruby, its rich metaprogramming capabilities got me thinking: What if we start with an empty Ruby object, and as we call methods on it, it uses an LLM to write and evaluate its own code on the fly? 1. The Blank Slate Let's start with a completely empty class: class Dummy dummy.add(1, 2) "Missing method: add, [1, 2], {}" "Prompt: You are a Ruby code generator. A missing method add was called with the arguments: [Integer, Integer], keyword arguments: []. Return ONLY valid Ruby code defining this method. Do not include markdown formatting. Example: def add(...) # implementation end " "Code: def add(a, b) a + b end" => 3 In real-time, the LLM synthesized def add(a, b); a + b; end , registered it on dummy , executed it, and returned 3 . Subsequent calls to dummy.add(1, 2) will execute instantly without hitting method_missing again! 4. Adding Context: State and Inter-Method Dependencies A real object has multiple methods that need to share state via instance variables and interact with one another. To enable this, our LLM needs context about existing instance variables, their types, and previously generated methods. We introduce a helper method llm_context to capture this state: def llm_context context = "# Current Instance Variables and Types:\n" if instance_variables.empty? context += "(No instance variables yet)\n" else instance_variables.each do |name| value = instance_variable_get(name) context += " - #{name}: #{value.class.name}\n" end end context += "# Previously Generated Methods:\n" if @_generated_methods.nil? || @_generated_methods.empty? context += "(No generated methods yet)\n" else @_generated_methods.each do |name, code| context += "#{code}\n" end end context end We then update our prompt to include this rich context: prompt = dummy.name = "Onur" "Missing method: name=, ["Onur"], {}" "Prompt: You are a Ruby code generator. A missing method name= was called with the arguments: [String], keyword arguments: []. Write ONLY the valid Ruby code to define name=. Ensure it works well with the existing instance variables and previously generated methods. # Current Instance Variables and Types: - @chat: RubyLLM::Chat # Previously Generated Methods: (No generated methods yet) Return ONLY valid Ruby code defining this method. Do not include markdown formatting. Example: def name=(...) # implementation end " "Code: def name=(value) @name = value end" => "Onur" Calling dummy.name = "Onur" dynamically created the setter method name=(value) which initialized the @name instance variable. Step 2: Reading the Value irb(main):004> dummy.name "Missing method: name, [], {}" "Prompt: You are a Ruby code generator. A missing method name was called with the arguments: [], keyword arguments: []. Write ONLY the valid Ruby code to define name. Ensure it works well with the existing instance variables and previously generated methods. # Current Instance Variables and Types: - @chat: RubyLLM::Chat - @_generated_methods: Hash - @name: String # Previously Generated Methods: def name=(value) @name = value end Return ONLY valid Ruby code defining this method. Do not include markdown formatting. Example: def name(...) # implementation end " "Code: def name @name end" => "Onur" Because our prompt included @name: String and the previously defined name=(value) method, the LLM understood the context and generated the exact matching getter method def name; @name; end . What's Next? This experiment demonstrates how easily Ruby's dynamic nature combines with LLMs to build self-assembling objects. There are a few natural next steps for expanding this idea-such as persisting the generated code to disk, adding sandboxing/security checks, or enabling feedback loops to auto-fix runtime errors. Source Code & Examples The complete working source code for this post is available in the site repository: Top comments (0)
Comments
No comments yet. Start the discussion.