Commit 7dc4357

mo khan <mo@mokhan.ca>
2025-08-12 18:50:25
refactor: extract states for processing a task
1 parent 3ac1303
Changed files (3)
lib/elelem/agent.rb
@@ -6,7 +6,7 @@ module Elelem
 
     def initialize(configuration)
       @configuration = configuration
-      transition_to(Idle.new(configuration))
+      transition_to(Idle.new)
     end
 
     def transition_to(next_state)
@@ -21,6 +21,10 @@ module Elelem
       configuration.tui.say(message, colour: colour, newline: newline)
     end
 
+    def execute(tool_call)
+      configuration.tools.execute(tool_call)
+    end
+
     def quit
       exit
     end
lib/elelem/api.rb
@@ -8,14 +8,14 @@ module Elelem
       @configuration = configuration
     end
 
-    def chat(messages, tools)
+    def chat(messages)
       body = {
         messages: messages,
         model: configuration.model,
         stream: true,
         keep_alive: "5m",
         options: { temperature: 0.1 },
-        tools: tools.to_h
+        tools: configuration.tools.to_h
       }
       json_body = body.to_json
 
lib/elelem/state.rb
@@ -2,57 +2,111 @@
 
 module Elelem
   class Idle
-    attr_reader :configuration
-
-    def initialize(configuration)
-      @configuration = configuration
-    end
-
     def run(agent)
       input = agent.prompt("\n> ")
       agent.quit if input.nil? || input.empty? || input == "exit"
 
-      configuration.conversation.add(role: "user", content: input)
-      agent.transition_to(ProcessingInput.new(configuration))
+      agent.configuration.conversation.add(role: "user", content: input)
+      agent.transition_to(ProcessingInput.new)
     end
   end
 
   class ProcessingInput
-    attr_reader :configuration, :conversation, :tools
+    class Waiting
+      attr_reader :agent
+
+      def initialize(agent)
+        @agent = agent
+      end
 
-    def initialize(configuration)
-      @configuration = configuration
-      @conversation = configuration.conversation
-      @tools = configuration.tools
+      def process(message)
+        state = self
+
+        if message["thinking"]
+          state = Thinking.new(agent)
+        elsif message["tool_calls"]&.any?
+          state = Executing.new(agent)
+        elsif message["content"].to_s.strip
+          state = Talking.new(agent)
+        elsif message["done"]
+          state = nil
+        else
+          raise message.inspect
+        end
+
+        state&.process(message)
+      end
+    end
+
+    class Thinking
+      attr_reader :agent
+
+      def initialize(agent)
+        @agent = agent
+      end
+
+      def process(message)
+        if message["thinking"]
+          agent.say(message["thinking"], colour: :gray, newline: false)
+          self
+        else
+          agent.say("", newline: true)
+          Waiting.new(agent).process(message)
+        end
+      end
+    end
+
+    class Executing
+      attr_reader :agent
+
+      def initialize(agent)
+        @agent = agent
+      end
+
+      def process(message)
+        if message["tool_calls"]&.any?
+          message["tool_calls"].each do |tool_call|
+            agent.configuration.conversation.add(role: "tool", content: agent.execute(tool_call))
+          end
+        end
+
+        Waiting.new(agent)
+      end
+    end
+
+    class Talking
+      attr_reader :agent
+
+      def initialize(agent)
+        @agent = agent
+      end
+
+      def process(message)
+        if message["content"]
+          agent.say(message["content"], colour: :default, newline: false)
+          self
+        else
+          agent.say("", newline: true)
+          Waiting.new(agent).process(message)
+        end
+      end
     end
 
     def run(agent)
-      done = false
+      state = Waiting.new(agent)
 
       loop do
-        configuration.api.chat(conversation.history, tools) do |chunk|
+        agent.configuration.api.chat(agent.configuration.conversation.history) do |chunk|
           response = JSON.parse(chunk)
-          done = response["done"]
           message = response["message"] || {}
 
-          if message["thinking"]
-            agent.say(message["thinking"], colour: :gray, newline: false)
-          elsif message["tool_calls"]&.any?
-            message["tool_calls"].each do |t|
-              conversation.add(role: "tool", content: tools.execute(t))
-            end
-            done = false
-          elsif message["content"].to_s.strip
-            agent.say(message["content"], colour: :default, newline: false)
-          else
-            raise chunk.inspect
-          end
+          state = state.process(message)
         end
 
-        break if done
+        break if state.nil?
       end
 
-      agent.transition_to(Idle.new(configuration))
+      agent.transition_to(Idle.new)
     end
   end
 end