Commit 9788712

mo khan <mo@mokhan.ca>
2026-01-17 05:50:23
refactor: simplify shell execution and reduce toolset
- Merge sh/sh_stream into single streaming implementation with block-based output - Reduce tools from 5 to 3 (read, write, execute) - Execute tool now accepts full shell pipelines via `bash -c` - Change output format from string keys to `:output` symbol - Remove dead code (Elelem::Error, redundant Reline config) - Update system prompt with tool discovery hint
1 parent 2eecd69
lib/elelem/agent.rb
@@ -91,24 +91,24 @@ module Elelem
     end
 
     def format_tool_result(name, result)
-      text = result["stdout"] || result["stderr"] || result[:content] || result[:error] || ""
+      text = result[:output] || result[:content] || result[:error] || ""
       return nil if text.strip.empty?
 
       result[:error] ? "  ! #{text.lines.first&.strip}" : text
     end
 
     def truncate(result)
-      %w[stdout stderr].each do |k|
-        next unless result[k].is_a?(String) && result[k].lines.size > MAX_LINES
-        result[k] = result[k].lines.first(MAX_LINES).join + "… (truncated)"
-      end
+      return result unless result[:output].is_a?(String) && result[:output].lines.size > MAX_LINES
+
+      result[:output] = result[:output].lines.first(MAX_LINES).join + "… (truncated)"
       result
     end
 
     def system_prompt
       <<~PROMPT.strip
-        Terminal agent. Be concise. Act directly, verify your work. Stay grounded - only respond to what is asked.
+        Terminal agent. Be concise. Act directly, verify your work.
         pwd: #{Dir.pwd}
+        Use `which` or `compgen -c | grep` to discover available tools.
       PROMPT
     end
   end
lib/elelem/terminal.rb
@@ -74,7 +74,7 @@ module Elelem
 
     def complete_files(target)
       result = Elelem.sh("bash", args: ["-c", "compgen -f #{target}"])
-      result["stdout"].lines.map(&:strip).first(20)
+      result[:output].lines.map(&:strip).first(20)
     end
   end
 end
lib/elelem/toolbox.rb
@@ -15,27 +15,15 @@ module Elelem
         required: ["path", "content"],
         fn: ->(a) { p = Pathname.new(a["path"]).expand_path; FileUtils.mkdir_p(p.dirname); { bytes: p.write(a["content"]) } }
       },
-      "exec" => {
-        desc: "Run shell command",
-        params: { cmd: { type: "string" }, args: { type: "array", items: { type: "string" } }, stdin: { type: "string" } },
-        required: ["cmd"],
-        fn: ->(a) { Elelem.sh(a["cmd"], args: a["args"] || [], stdin: a["stdin"]) }
-      },
-      "grep" => {
-        desc: "Search git-tracked files",
-        params: { query: { type: "string" } },
-        required: ["query"],
-        fn: ->(a) { Elelem.sh("git", args: ["grep", "-nI", a["query"]]) }
-      },
-      "list" => {
-        desc: "List git-tracked files",
-        params: { path: { type: "string" } },
-        required: [],
-        fn: ->(a) { Elelem.sh("git", args: a["path"] ? ["ls-files", "--", a["path"]] : ["ls-files"]) }
+      "execute" => {
+        desc: "Run shell command (supports pipes and redirections)",
+        params: { command: { type: "string" } },
+        required: ["command"],
+        fn: ->(a) { Elelem.sh("bash", args: ["-c", a["command"]]) { |x| $stdout.print(x) } }
       }
     }.freeze
 
-    ALIASES = { "bash" => "exec", "sh" => "exec", "open" => "read" }.freeze
+    ALIASES = { "bash" => "execute", "sh" => "execute", "exec" => "execute", "open" => "read" }.freeze
 
     attr_reader :tools
 
lib/elelem.rb
@@ -5,6 +5,7 @@ require "json"
 require "net/llm"
 require "open3"
 require "pathname"
+require "stringio"
 require "reline"
 
 require_relative "elelem/agent"
@@ -12,15 +13,17 @@ require_relative "elelem/terminal"
 require_relative "elelem/toolbox"
 require_relative "elelem/version"
 
-Reline.input = $stdin
-Reline.output = $stdout
-
 module Elelem
-  class Error < StandardError; end
-
-  def self.sh(cmd, args: [], env: {}, cwd: Dir.pwd, stdin: nil)
-    stdout, stderr, status = Open3.capture3(env, cmd, *args, chdir: cwd, stdin_data: stdin)
-    { "exit_status" => status.exitstatus, "stdout" => stdout, "stderr" => stderr }
+  def self.sh(cmd, args: [], cwd: Dir.pwd)
+    output = StringIO.new
+    Open3.popen2e(cmd, *args, chdir: cwd) do |stdin, out, wait_thr|
+      stdin.close
+      out.each_line do |l|
+        yield l if block_given?
+        output.write(l)
+      end
+      { exit_status: wait_thr.value.exitstatus, output: output.string }
+    end
   end
 
   def self.start(client)
spec/elelem/toolbox_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Elelem::Toolbox do
   describe "#to_h" do
     it "returns all tools in API format" do
       tool_names = subject.to_h.map { |t| t.dig(:function, :name) }
-      expect(tool_names).to include("read", "write", "exec", "grep", "list")
+      expect(tool_names).to include("read", "write", "execute")
     end
   end
 
@@ -25,10 +25,5 @@ RSpec.describe Elelem::Toolbox do
       result = subject.run("nonexistent", {})
       expect(result[:error]).to include("unknown tool")
     end
-
-    it "executes grep tool" do
-      result = subject.run("grep", { "query" => "RSpec.describe" })
-      expect(result["stdout"]).to include("toolbox_spec.rb")
-    end
   end
 end