Commit 98c664e

mo khan <mo@mokhan.ca>
2025-08-20 21:08:38
feat: add autonomous coding tools and planning state machine
1 parent 293ba52
lib/elelem/states/working.rb
@@ -5,14 +5,12 @@ module Elelem
     module Working
       class << self
         def run(agent)
-          done = false
           state = Waiting.new(agent)
 
           loop do
             agent.api.chat(agent.conversation.history) do |chunk|
               response = JSON.parse(chunk)
               message = normalize(response["message"] || {})
-              done = response["done"]
 
               agent.logger.debug("#{state.display_name}: #{message}")
               state = state.run(message)
lib/elelem/toolbox/file.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class File < ::Elelem::Tool
+      attr_reader :tui
+
+      def initialize(configuration)
+        @tui = configuration.tui
+        super("file", "Read/write files in project directory", {
+          type: "object",
+          properties: {
+            action: {
+              type: "string",
+              enum: ["read", "write", "append"],
+              description: "File operation to perform"
+            },
+            path: {
+              type: "string",
+              description: "Relative path to file from project root"
+            },
+            content: {
+              type: "string",
+              description: "Content to write/apppend (only for write/append actions)"
+            }
+          },
+          required: ["action", "path"]
+        })
+      end
+
+      def call(args)
+        path = Pathname.pwd.join(args["path"])
+        case args["action"]
+        when "read"
+          path.read
+        when "write"
+          path.write(args["content"])
+          "File written successfully"
+        when "append"
+          path.open("a") { |f| f << args["content"] }
+          "Content appended successfully"
+        end
+      rescue => e
+        e.message
+      end
+    end
+  end
+end
lib/elelem/toolbox/git.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class Git < ::Elelem::Tool
+      def initialize(configuration)
+        @configuration = configuration
+        super("git", "Perform git operations on repository", {
+          type: "object",
+          properties: {
+            action: {
+              type: "string",
+              enum: ["commit", "diff", "log"],
+              description: "Git operation to perform"
+            },
+            message: {
+              type: "string",
+              description: "Commit message (required for commit action)"
+            }
+          },
+          required: ["action"]
+        })
+      end
+
+      def call(args)
+        case args["action"]
+        when "commit"
+          `git add . && git commit -m "#{args["message"]}"`
+          "Committed changes: #{args["message"]}"
+        when "diff"
+          `git diff HEAD`
+        when "log"
+          `git log --oneline -n 10`
+        end
+      rescue => e
+        e.message
+      end
+    end
+  end
+end
lib/elelem/toolbox/prompt.rb
@@ -3,7 +3,7 @@
 module Elelem
   module Toolbox
     class Prompt < Tool
-      def initialize
+      def initialize(configuration)
         super(
           "prompt",
           "Ask the user a question and get their response.",
lib/elelem/toolbox/search.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Elelem
+  module Toolbox
+    class Search < ::Elelem::Tool
+      def initialize(configuration)
+        @configuration = configuration
+        super("search", "Search files in project directory", {
+          type: "object",
+          properties: {
+            pattern: {
+              type: "string",
+              description: "Search pattern (grep compatible)"
+            },
+            path: {
+              type: "string",
+              description: "Directory path to search from (default: project root)"
+            }
+          },
+          required: ["pattern"]
+        })
+      end
+
+      def call(args)
+        path = args["path"] || "."
+        `grep -rnw '#{args["pattern"]}' #{path}`
+      rescue => e
+        e.message
+      end
+    end
+  end
+end
lib/elelem/configuration.rb
@@ -37,7 +37,15 @@ module Elelem
     end
 
     def tools
-      @tools ||= Tools.new(self, [Toolbox::Bash.new(self), Toolbox::Prompt.new] + mcp_tools)
+      @tools ||= Tools.new(self,
+        [
+          Toolbox::Bash.new(self),
+          Toolbox::File.new(self),
+          Toolbox::Git.new(self),
+          Toolbox::Prompt.new(self),
+          Toolbox::Search.new(self),
+        ] + mcp_tools
+      )
     end
 
     def cleanup
lib/elelem/toolbox.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+require_relative "toolbox/bash"
+require_relative "toolbox/file"
+require_relative "toolbox/git"
+require_relative "toolbox/mcp"
+require_relative "toolbox/prompt"
+require_relative "toolbox/search"
lib/elelem.rb
@@ -1,14 +1,11 @@
 # frozen_string_literal: true
 
-# require "base64"
 require "cli/ui"
-# require "ed25519"
 require "erb"
 require "json"
 require "json-schema"
 require "logger"
 require "net/http"
-# require "net/ssh"
 require "open3"
 require "reline"
 require "thor"
@@ -30,9 +27,7 @@ require_relative "elelem/states/working/talking"
 require_relative "elelem/states/working/thinking"
 require_relative "elelem/states/working/waiting"
 require_relative "elelem/tool"
-require_relative "elelem/toolbox/bash"
-require_relative "elelem/toolbox/mcp"
-require_relative "elelem/toolbox/prompt"
+require_relative "elelem/toolbox"
 require_relative "elelem/tools"
 require_relative "elelem/tui"
 require_relative "elelem/version"
sig/elelem.rbs
@@ -1,4 +0,0 @@
-module Elelem
-  VERSION: String
-  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
-end
elelem.gemspec
@@ -50,27 +50,27 @@ Gem::Specification.new do |spec|
     "lib/elelem/states/working/waiting.rb",
     "lib/elelem/system_prompt.erb",
     "lib/elelem/tool.rb",
+    "lib/elelem/toolbox.rb",
     "lib/elelem/toolbox/bash.rb",
+    "lib/elelem/toolbox/file.rb",
+    "lib/elelem/toolbox/git.rb",
     "lib/elelem/toolbox/mcp.rb",
+    "lib/elelem/toolbox/prompt.rb",
+    "lib/elelem/toolbox/search.rb",
     "lib/elelem/tools.rb",
     "lib/elelem/tui.rb",
     "lib/elelem/version.rb",
-    "sig/elelem.rbs"
   ]
   spec.bindir = "exe"
   spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
   spec.require_paths = ["lib"]
 
-  # spec.add_dependency "base64"
-  # spec.add_dependency "bcrypt_pbkdf"
   spec.add_dependency "cli-ui"
-  # spec.add_dependency "ed25519"
   spec.add_dependency "erb"
   spec.add_dependency "json"
   spec.add_dependency "json-schema"
   spec.add_dependency "logger"
   spec.add_dependency "net-http"
-  # spec.add_dependency "net-ssh"
   spec.add_dependency "open3"
   spec.add_dependency "reline"
   spec.add_dependency "thor"
README.md
@@ -58,100 +58,6 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
 
 To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
-REPL State Diagram
-
-```
-                      ┌─────────────────┐
-                      │   START/INIT    │
-                      └─────────┬───────┘
-                                │
-                                v
-                      ┌─────────────────┐
-                ┌────▶│ IDLE (Prompt)   │◄────┐
-                │     │   Shows "> "    │     │
-                │     └─────────┬───────┘     │
-                │               │             │
-                │               │ User input  │
-                │               v             │
-                │     ┌─────────────────┐     │
-                │     │ PROCESSING      │     │
-                │     │ INPUT           │     │
-                │     └─────────┬───────┘     │
-                │               │             │
-                │               │ API call    │
-                │               v             │
-                │     ┌─────────────────┐     │
-                │     │ STREAMING       │     │
-                │ ┌──▶│ RESPONSE        │─────┤
-                │ │   └─────────┬───────┘     │
-                │ │             │             │ done=true
-                │ │             │ Parse chunk │
-                │ │             v             │
-                │ │   ┌─────────────────┐     │
-                │ │   │ MESSAGE TYPE    │     │
-                │ │   │ ROUTING         │     │
-                │ │   └─────┬─┬─┬───────┘     │
-                │ │         │ │ │             │
-       ┌────────┴─┴─────────┘ │ └─────────────┴──────────┐
-       │                      │                          │
-       v                      v                          v
-  ┌─────────────┐    ┌─────────────┐          ┌─────────────┐
-  │ THINKING    │    │ TOOL        │          │ CONTENT     │
-  │ STATE       │    │ EXECUTION   │          │ OUTPUT      │
-  │             │    │ STATE       │          │ STATE       │
-  └─────────────┘    └─────┬───────┘          └─────────────┘
-       │                   │                          │
-       │                   │ done=false               │
-       └───────────────────┼──────────────────────────┘
-                           │
-                           v
-                 ┌─────────────────┐
-                 │ CONTINUE        │
-                 │ STREAMING       │
-                 └─────────────────┘
-                           │
-                           └─────────────────┐
-                                             │
-       ┌─────────────────┐                   │
-       │ ERROR STATE     │                   │
-       │ (Exception)     │                   │
-       └─────────────────┘                   │
-                ▲                            │
-                │ Invalid response           │
-                └────────────────────────────┘
-
-                      EXIT CONDITIONS:
-                 ┌─────────────────────────┐
-                 │ • User enters ""        │
-                 │ • User enters "exit"    │
-                 │ • EOF (Ctrl+D)          │
-                 │ • nil input             │
-                 └─────────────────────────┘
-                            │
-                            v
-                 ┌─────────────────────────┐
-                 │      TERMINATE          │
-                 └─────────────────────────┘
-```
-
-Key Transitions:
-
-1. IDLE → PROCESSING: User enters any non-empty, non-"exit" input
-2. PROCESSING → STREAMING: API call initiated to Ollama
-3. STREAMING → MESSAGE ROUTING: Each chunk received is parsed
-4. MESSAGE ROUTING → States: Based on message content:
-  - thinking → THINKING STATE
-  - tool_calls → TOOL EXECUTION STATE
-  - content → CONTENT OUTPUT STATE
-  - Invalid format → ERROR STATE
-5. All States → IDLE: When done=true from API response
-6. TOOL EXECUTION → STREAMING: Sets done=false to continue conversation
-7. Any State → TERMINATE: On exit conditions
-
-The REPL operates as a continuous loop where the primary flow is IDLE → PROCESSING → STREAMING →
-back to IDLE, with the streaming phase potentially cycling through multiple message types before
-completion.
-
 ## Contributing
 
 Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.