Commit ca54124
Changed files (12)
lib/elelem/agent.rb
@@ -5,11 +5,10 @@ module Elelem
PROVIDERS = %w[ollama anthropic openai vertex-ai].freeze
ANTHROPIC_MODELS = %w[claude-sonnet-4-20250514 claude-opus-4-20250514 claude-haiku-3-5-20241022].freeze
VERTEX_MODELS = %w[claude-sonnet-4@20250514 claude-opus-4-5@20251101].freeze
- COMMANDS = %w[/env /mode /provider /model /shell /clear /context /exit /help].freeze
- MODES = %w[auto build plan verify].freeze
+ COMMANDS = %w[/env /provider /model /shell /clear /context /exit /help].freeze
ENV_VARS = %w[ANTHROPIC_API_KEY OPENAI_API_KEY OPENAI_BASE_URL OLLAMA_HOST GOOGLE_CLOUD_PROJECT GOOGLE_CLOUD_REGION].freeze
- attr_reader :conversation, :client, :toolbox, :provider, :terminal, :permissions
+ attr_reader :conversation, :client, :toolbox, :provider, :terminal
def initialize(provider, model, toolbox, terminal: nil)
@conversation = Conversation.new
@@ -17,18 +16,17 @@ module Elelem
@toolbox = toolbox
@client = build_client(provider, model)
@terminal = terminal || default_terminal
- @permissions = Set.new([:read])
end
def repl
loop do
- input = terminal.ask("User> ")
+ input = terminal.ask("> ")
break if input.nil?
if input.start_with?("/")
handle_slash_command(input)
else
conversation.add(role: :user, content: input)
- result = execute_turn(conversation.history_for(permissions))
+ result = execute_turn(conversation.history)
conversation.add(role: result[:role], content: result[:content])
end
end
@@ -40,37 +38,18 @@ module Elelem
Terminal.new(
commands: COMMANDS,
env_vars: ENV_VARS,
- modes: MODES,
providers: PROVIDERS
)
end
def handle_slash_command(input)
case input
- when "/mode auto"
- permissions.replace([:read, :write, :execute])
- terminal.say " → Mode: auto (all tools enabled)"
- when "/mode build"
- permissions.replace([:read, :write])
- terminal.say " → Mode: build (read + write)"
- when "/mode plan"
- permissions.replace([:read])
- terminal.say " → Mode: plan (read-only)"
- when "/mode verify"
- permissions.replace([:read, :execute])
- terminal.say " → Mode: verify (read + execute)"
- when "/mode"
- terminal.say " Usage: /mode [auto|build|plan|verify]"
- terminal.say ""
- terminal.say " Provider: #{provider}/#{client.model}"
- terminal.say " Permissions: #{permissions.to_a.inspect}"
- terminal.say " Tools: #{toolbox.tools_for(permissions).map { |t| t.dig(:function, :name) }}"
when "/exit" then exit
when "/clear"
conversation.clear
terminal.say " → Conversation cleared"
when "/context"
- terminal.say conversation.dump(permissions), markdown: true
+ terminal.say conversation.dump, markdown: true
when "/shell"
transcript = start_shell
conversation.add(role: :user, content: transcript) unless transcript.strip.empty?
@@ -136,7 +115,6 @@ module Elelem
def help_banner
<<~HELP
/env VAR cmd...
- /mode auto build plan verify
/provider
/model
/shell
@@ -230,7 +208,7 @@ module Elelem
end
def execute_turn(messages)
- tools = toolbox.tools_for(permissions)
+ tools = toolbox.tools
turn_context = []
errors = 0
@@ -243,7 +221,6 @@ module Elelem
client.fetch(messages + turn_context, tools) do |chunk|
case chunk[:type]
when :delta
- terminal.write chunk[:thinking] if chunk[:thinking]
content += chunk[:content] if chunk[:content]
when :complete
content = chunk[:content] if chunk[:content]
@@ -255,7 +232,7 @@ module Elelem
return { role: "assistant", content: "[Error: #{e.message}]" }
end
- terminal.say("\nAssistant> #{content}", markdown: true) unless content.to_s.empty?
+ terminal.say("\n#{content}", markdown: true) unless content.to_s.empty?
api_tool_calls = tool_calls.any? ? format_tool_calls_for_api(tool_calls) : nil
turn_context << { role: "assistant", content: content, tool_calls: api_tool_calls }.compact
@@ -263,7 +240,7 @@ module Elelem
tool_calls.each do |call|
name, args = call[:name], call[:arguments]
terminal.say "\nTool> #{name}(#{args})"
- result = toolbox.run_tool(name, args, permissions: permissions)
+ result = toolbox.run_tool(name, args)
terminal.say truncate_output(format_tool_call_result(result))
turn_context << { role: "tool", tool_call_id: call[:id], content: JSON.dump(result) }
errors += 1 if result[:error]
lib/elelem/conversation.rb
@@ -8,10 +8,8 @@ module Elelem
@items = items
end
- def history_for(permissions)
- history = @items.dup
- history[0] = { role: "system", content: system_prompt_for(permissions) }
- history
+ def history
+ @items.dup
end
def add(role: :user, content: "")
@@ -30,39 +28,16 @@ module Elelem
@items = default_context
end
- def dump(permissions)
- history_for(permissions).map do |item|
+ def dump
+ history.map do |item|
"## #{item[:role].to_s.capitalize}\n\n#{item[:content]}"
end.join("\n\n---\n\n")
end
private
- def default_context(prompt = system_prompt_for([]))
- [{ role: "system", content: prompt }]
- end
-
- def system_prompt_for(permissions)
- base = system_prompt
-
- case permissions.sort
- when [:read]
- "#{base}\n\nYou may read files on the system."
- when [:write]
- "#{base}\n\nYou may write files on the system."
- when [:execute]
- "#{base}\n\nYou may execute shell commands on the system."
- when [:read, :write]
- "#{base}\n\nYou may read and write files on the system."
- when [:execute, :read]
- "#{base}\n\nYou may execute shell commands and read files on the system."
- when [:execute, :write]
- "#{base}\n\nYou may execute shell commands and write files on the system."
- when [:execute, :read, :write]
- "#{base}\n\nYou may read files, write files and execute shell commands on the system."
- else
- base
- end
+ def default_context
+ [{ role: "system", content: system_prompt }]
end
def system_prompt
lib/elelem/git_context.rb
@@ -1,79 +0,0 @@
-# frozen_string_literal: true
-
-module Elelem
- class GitContext
- MAX_DIFF_LINES = 100
-
- def initialize(shell = Elelem.shell)
- @shell = shell
- end
-
- def to_s
- return "" unless git_repo?
-
- parts = []
- parts << "Branch: #{branch}" if branch
- parts << status_section if status.any?
- parts << diff_section if staged_diff.any? || unstaged_diff.any?
- parts << recent_commits_section if recent_commits.any?
- parts.join("\n\n")
- end
-
- private
-
- def git_repo?
- @shell.execute("git", args: ["rev-parse", "--git-dir"])["exit_status"].zero?
- end
-
- def branch
- @branch ||= @shell.execute("git", args: ["branch", "--show-current"])["stdout"].strip.then { |b| b.empty? ? nil : b }
- end
-
- def status
- @status ||= @shell.execute("git", args: ["status", "--porcelain"])["stdout"].lines.map(&:chomp)
- end
-
- def staged_diff
- @staged_diff ||= @shell.execute("git", args: ["diff", "--cached", "--stat"])["stdout"].lines
- end
-
- def unstaged_diff
- @unstaged_diff ||= @shell.execute("git", args: ["diff", "--stat"])["stdout"].lines
- end
-
- def recent_commits
- @recent_commits ||= @shell.execute("git", args: ["log", "--oneline", "-5"])["stdout"].lines.map(&:strip)
- end
-
- def status_section
- modified = status.select { |l| l[0] == "M" || l[1] == "M" }.map { |l| l[3..] }
- added = status.select { |l| l[0] == "A" || l.start_with?("??") }.map { |l| l[3..] }
- deleted = status.select { |l| l[0] == "D" || l[1] == "D" }.map { |l| l[3..] }
-
- lines = []
- lines << "Modified: #{modified.join(', ')}" if modified.any?
- lines << "Added: #{added.join(', ')}" if added.any?
- lines << "Deleted: #{deleted.join(', ')}" if deleted.any?
- lines.any? ? "Working tree:\n#{lines.join("\n")}" : nil
- end
-
- def diff_section
- lines = []
- lines << "Staged:\n#{truncate(staged_diff)}" if staged_diff.any?
- lines << "Unstaged:\n#{truncate(unstaged_diff)}" if unstaged_diff.any?
- lines.join("\n\n")
- end
-
- def recent_commits_section
- "Recent commits:\n#{recent_commits.join("\n")}"
- end
-
- def truncate(lines)
- if lines.size > MAX_DIFF_LINES
- lines.first(MAX_DIFF_LINES).join + "\n... (#{lines.size - MAX_DIFF_LINES} more lines)"
- else
- lines.join
- end
- end
- end
-end
lib/elelem/system_prompt.erb
@@ -10,7 +10,3 @@ You are a trusted terminal agent. You act on behalf of the user - executing task
## System
<%= `uname -s`.strip %> · <%= ENV['PWD'] %>
-
-## Git State
-
-<%= Elelem::GitContext.new.to_s %>
lib/elelem/terminal.rb
@@ -2,9 +2,8 @@
module Elelem
class Terminal
- def initialize(commands: [], modes: [], providers: [], env_vars: [])
+ def initialize(commands: [], providers: [], env_vars: [])
@commands = commands
- @modes = modes
@providers = providers
@env_vars = env_vars
@spinner_thread = nil
@@ -73,8 +72,6 @@ module Elelem
end
case preposing.strip
- when '/mode'
- @modes.select { |m| m.start_with?(target) }
when '/provider'
@providers.select { |p| p.start_with?(target) }
when '/env'
lib/elelem/toolbox.rb
@@ -26,7 +26,7 @@ module Elelem
Elelem.shell.execute("git", args: args["path"] ? ["ls-files", "--", args["path"]] : ["ls-files"])
end
- PATCH_TOOL = Tool.build( "patch", "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.", { diff: { type: "string" } }, ["diff"]) do |args|
+ PATCH_TOOL = Tool.build("patch", "Apply a unified diff patch via 'git apply'. Use for surgical edits to existing files.", { diff: { type: "string" } }, ["diff"]) do |args|
Elelem.shell.execute("git", args: ["apply", "--index", "--whitespace=nowarn", "-p1"], stdin: args["diff"])
end
@@ -63,47 +63,36 @@ module Elelem
"web" => "fetch",
}
- attr_reader :tools
-
def initialize
@tools_by_name = {}
- @tool_permissions = {}
- @tools = { read: [], write: [], execute: [] }
- add_tool(eval_tool(binding), :execute)
- add_tool(WEB_SEARCH_TOOL, :read)
- add_tool(EXEC_TOOL, :execute)
- add_tool(FETCH_TOOL, :read)
- add_tool(GREP_TOOL, :read)
- add_tool(LIST_TOOL, :read)
- add_tool(PATCH_TOOL, :write)
- add_tool(READ_TOOL, :read)
- add_tool(WRITE_TOOL, :write)
+ add_tool(eval_tool(binding))
+ add_tool(EXEC_TOOL)
+ add_tool(FETCH_TOOL)
+ add_tool(GREP_TOOL)
+ add_tool(LIST_TOOL)
+ add_tool(PATCH_TOOL)
+ add_tool(READ_TOOL)
+ add_tool(WEB_SEARCH_TOOL)
+ add_tool(WRITE_TOOL)
end
- def add_tool(tool, permission)
- @tools[permission] << tool
+ def add_tool(tool)
@tools_by_name[tool.name] = tool
- @tool_permissions[tool.name] = permission
end
- def register_tool(name, description, properties = {}, required = [], mode: :execute, &block)
- add_tool(Tool.build(name, description, properties, required, &block), mode)
+ def register_tool(name, description, properties = {}, required = [], &block)
+ add_tool(Tool.build(name, description, properties, required, &block))
end
- def tools_for(permissions)
- Array(permissions).map { |permission| tools[permission].map(&:to_h) }.flatten
+ def tools
+ @tools_by_name.values.map(&:to_h)
end
- def run_tool(name, args, permissions: [])
+ def run_tool(name, args)
resolved_name = TOOL_ALIASES.fetch(name, name)
tool = @tools_by_name[resolved_name]
return { error: "Unknown tool", name: name, args: args } unless tool
- tool_permission = @tool_permissions[resolved_name]
- unless Array(permissions).include?(tool_permission)
- return { error: "Tool '#{resolved_name}' not available in current mode", name: name }
- end
-
tool.call(args)
rescue => error
{ error: error.message, name: name, args: args, backtrace: error.backtrace.first(5) }
@@ -116,7 +105,7 @@ module Elelem
private
def eval_tool(target_binding)
- Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required, mode: :execute) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
+ Tool.build("eval", "Evaluates Ruby code with full access to register new tools via the `register_tool(name, desc, properties, required) { |args| ... }` method.", { ruby: { type: "string" } }, ["ruby"]) do |args|
{ result: target_binding.eval(args["ruby"]) }
end
end
lib/elelem.rb
@@ -20,7 +20,6 @@ require "tty/markdown"
require_relative "elelem/agent"
require_relative "elelem/application"
require_relative "elelem/conversation"
-require_relative "elelem/git_context"
require_relative "elelem/terminal"
require_relative "elelem/tool"
require_relative "elelem/toolbox"
spec/elelem/agent_e2e_spec.rb
@@ -9,53 +9,6 @@ RSpec.describe Elelem::Agent do
end
describe "slash commands" do
- describe "/mode" do
- it "shows help when called without arguments" do
- terminal = Elelem::FakeTerminal.new(inputs: ["/mode", nil])
- agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
- agent.repl
-
- expect(terminal.output).to include(" Usage: /mode [auto|build|plan|verify]")
- end
-
- it "switches to auto mode" do
- terminal = Elelem::FakeTerminal.new(inputs: ["/mode auto", nil])
- agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
- agent.repl
-
- expect(terminal.output).to include(" → Mode: auto (all tools enabled)")
- end
-
- it "switches to build mode" do
- terminal = Elelem::FakeTerminal.new(inputs: ["/mode build", nil])
- agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
- agent.repl
-
- expect(terminal.output).to include(" → Mode: build (read + write)")
- end
-
- it "switches to plan mode" do
- terminal = Elelem::FakeTerminal.new(inputs: ["/mode plan", nil])
- agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
- agent.repl
-
- expect(terminal.output).to include(" → Mode: plan (read-only)")
- end
-
- it "switches to verify mode" do
- terminal = Elelem::FakeTerminal.new(inputs: ["/mode verify", nil])
- agent = described_class.new("ollama", nil, toolbox, terminal: terminal)
-
- agent.repl
-
- expect(terminal.output).to include(" → Mode: verify (read + execute)")
- end
- end
-
describe "/clear" do
it "clears the conversation" do
terminal = Elelem::FakeTerminal.new(inputs: ["/clear", nil])
@@ -98,7 +51,8 @@ RSpec.describe Elelem::Agent do
agent.repl
expect(terminal.output.join).to include("/env VAR cmd...")
- expect(terminal.output.join).to include("/mode auto build plan verify")
+ expect(terminal.output.join).to include("/provider")
+ expect(terminal.output.join).to include("/clear")
end
end
end
spec/elelem/agent_spec.rb
@@ -20,24 +20,9 @@ RSpec.describe Elelem::Agent do
expect(agent.client).to eq(mock_client)
end
- it "initializes tools for all modes" do
- expect(agent.toolbox.tools[:read]).to be_an(Array)
- expect(agent.toolbox.tools[:write]).to be_an(Array)
- expect(agent.toolbox.tools[:execute]).to be_an(Array)
- end
- end
-
- describe "integration with conversation" do
- it "conversation uses mode-aware prompts" do
- conversation = agent.conversation
- conversation.add(role: :user, content: "test message")
-
- read_history = conversation.history_for([:read])
- write_history = conversation.history_for([:write])
-
- expect(read_history[0][:content]).to include("You may read files on the system")
- expect(write_history[0][:content]).to include("You may write files on the system")
- expect(read_history[0][:content]).not_to eq(write_history[0][:content])
+ it "initializes toolbox with all tools" do
+ tool_names = agent.toolbox.tools.map { |t| t.dig(:function, :name) }
+ expect(tool_names).to include("read", "write", "exec", "grep", "list")
end
end
end
spec/elelem/conversation_spec.rb
@@ -3,90 +3,13 @@
RSpec.describe Elelem::Conversation do
let(:conversation) { described_class.new }
- describe "#history_for" do
- context "with empty conversation" do
- it "returns history with mode-specific system prompt for read mode" do
- history = conversation.history_for([:read])
-
- expect(history.length).to eq(1)
- expect(history[0][:role]).to eq("system")
- expect(history[0][:content]).to include("You may read files on the system")
- end
-
- it "returns history with mode-specific system prompt for write mode" do
- history = conversation.history_for([:write])
-
- expect(history[0][:content]).to include("You may write files on the system")
- end
-
- it "returns history with mode-specific system prompt for execute mode" do
- history = conversation.history_for([:execute])
-
- expect(history[0][:content]).to include("You may execute shell commands on the system")
- end
-
- it "returns history with mode-specific system prompt for read+write mode" do
- history = conversation.history_for([:read, :write])
-
- expect(history[0][:content]).to include("You may read and write files on the system")
- end
-
- it "returns history with mode-specific system prompt for read+execute mode" do
- history = conversation.history_for([:read, :execute])
+ describe "#history" do
+ it "returns history with system prompt" do
+ history = conversation.history
- expect(history[0][:content]).to include("You may execute shell commands and read files on the system")
- end
-
- it "returns history with mode-specific system prompt for write+execute mode" do
- history = conversation.history_for([:write, :execute])
-
- expect(history[0][:content]).to include("You may execute shell commands and write files on the system")
- end
-
- it "returns history with mode-specific system prompt for all tools mode" do
- history = conversation.history_for([:read, :write, :execute])
-
- expect(history[0][:content]).to include("You may read files, write files and execute shell commands on the system")
- end
-
- it "returns base system prompt for unknown mode" do
- history = conversation.history_for([:unknown])
-
- expect(history[0][:content]).not_to include("Read and analyze")
- expect(history[0][:content]).not_to include("Write clean")
- end
-
- it "returns base system prompt for empty mode" do
- history = conversation.history_for([])
-
- expect(history[0][:role]).to eq("system")
- expect(history[0][:content]).to be_a(String)
- end
- end
-
- context "with mode order independence" do
- it "returns same prompt for [:read, :write] and [:write, :read]" do
- history1 = conversation.history_for([:read, :write])
- history2 = conversation.history_for([:write, :read])
-
- expect(history1[0][:content]).to eq(history2[0][:content])
- end
-
- it "returns same prompt for [:read, :execute] and [:execute, :read]" do
- history1 = conversation.history_for([:read, :execute])
- history2 = conversation.history_for([:execute, :read])
-
- expect(history1[0][:content]).to eq(history2[0][:content])
- end
-
- it "returns same prompt for all permutations of [:read, :write, :execute]" do
- history1 = conversation.history_for([:read, :write, :execute])
- history2 = conversation.history_for([:execute, :read, :write])
- history3 = conversation.history_for([:write, :execute, :read])
-
- expect(history1[0][:content]).to eq(history2[0][:content])
- expect(history2[0][:content]).to eq(history3[0][:content])
- end
+ expect(history.length).to eq(1)
+ expect(history[0][:role]).to eq("system")
+ expect(history[0][:content]).to be_a(String)
end
context "with populated conversation" do
@@ -96,7 +19,7 @@ RSpec.describe Elelem::Conversation do
end
it "preserves all conversation items" do
- history = conversation.history_for([:read])
+ history = conversation.history
expect(history.length).to eq(3)
expect(history[1][:role]).to eq(:user)
@@ -105,18 +28,8 @@ RSpec.describe Elelem::Conversation do
expect(history[2][:content]).to eq("Hi there")
end
- it "updates system prompt without mutating original" do
- original_items = conversation.instance_variable_get(:@items)
- original_system_content = original_items[0][:content]
-
- history = conversation.history_for([:read])
-
- expect(history[0][:content]).not_to eq(original_system_content)
- expect(original_items[0][:content]).to eq(original_system_content)
- end
-
it "returns a copy, not the original array" do
- history = conversation.history_for([:read])
+ history = conversation.history
original_items = conversation.instance_variable_get(:@items)
expect(history).not_to be(original_items)
@@ -127,7 +40,7 @@ RSpec.describe Elelem::Conversation do
describe "#add" do
it "adds user message to conversation" do
conversation.add(role: :user, content: "test message")
- history = conversation.history_for([])
+ history = conversation.history
expect(history.length).to eq(2)
expect(history[1][:content]).to eq("test message")
@@ -136,7 +49,7 @@ RSpec.describe Elelem::Conversation do
it "merges consecutive messages with same role" do
conversation.add(role: :user, content: "part 1")
conversation.add(role: :user, content: "part 2")
- history = conversation.history_for([])
+ history = conversation.history
expect(history.length).to eq(2)
expect(history[1][:content]).to eq("part 1part 2")
@@ -144,14 +57,14 @@ RSpec.describe Elelem::Conversation do
it "ignores nil content" do
conversation.add(role: :user, content: nil)
- history = conversation.history_for([])
+ history = conversation.history
expect(history.length).to eq(1)
end
it "ignores empty content" do
conversation.add(role: :user, content: "")
- history = conversation.history_for([])
+ history = conversation.history
expect(history.length).to eq(1)
end
@@ -167,7 +80,7 @@ RSpec.describe Elelem::Conversation do
it "resets conversation to default context" do
conversation.add(role: :user, content: "test")
conversation.clear
- history = conversation.history_for([])
+ history = conversation.history
expect(history.length).to eq(1)
expect(history[0][:role]).to eq("system")
@@ -175,13 +88,12 @@ RSpec.describe Elelem::Conversation do
end
describe "#dump" do
- it "returns markdown representation with mode-specific prompt" do
+ it "returns markdown representation" do
conversation.add(role: :user, content: "test")
- result = conversation.dump([:read])
+ result = conversation.dump
expect(result).to include("## System")
expect(result).to include("## User")
- expect(result).to include("You may read files on the system")
end
end
end
spec/elelem/toolbox_spec.rb
@@ -1,61 +1,16 @@
# frozen_string_literal: true
-#
+
RSpec.describe Elelem::Toolbox do
subject { described_class.new }
- describe "#tools_for" do
- it "returns read tools for read mode" do
- mode = Set[:read]
- tools = subject.tools_for(mode)
-
- tool_names = tools.map { |t| t.dig(:function, :name) }
- expect(tool_names).to include("grep", "list", "read", "fetch", "web_search")
- expect(tool_names).not_to include("write", "patch", "exec")
- end
-
- it "returns write tools for write mode" do
- mode = Set[:write]
- tools = subject.tools_for(mode)
-
- tool_names = tools.map { |t| t.dig(:function, :name) }
- expect(tool_names).to include("patch", "write")
- expect(tool_names).not_to include("grep", "exec")
- end
-
- it "returns execute tools for execute mode" do
- mode = Set[:execute]
- tools = subject.tools_for(mode)
-
- tool_names = tools.map { |t| t.dig(:function, :name) }
- expect(tool_names).to include("exec")
- expect(tool_names).not_to include("grep", "write")
- end
-
- it "returns all tools for auto mode" do
- mode = Set[:read, :write, :execute]
- tools = subject.tools_for(mode)
-
- tool_names = tools.map { |t| t.dig(:function, :name) }
- expect(tool_names).to include("grep", "list", "read", "patch", "write", "exec", "fetch", "web_search")
- end
-
- it "returns combined tools for build mode" do
- mode = Set[:read, :write]
- tools = subject.tools_for(mode)
-
- tool_names = tools.map { |t| t.dig(:function, :name) }
- expect(tool_names).to include("grep", "read", "write", "patch", "fetch", "web_search")
- expect(tool_names).not_to include("exec")
+ describe "#tools" do
+ it "returns all tools" do
+ tool_names = subject.tools.map { |t| t.dig(:function, :name) }
+ expect(tool_names).to include("grep", "list", "read", "patch", "write", "exec", "fetch", "web_search", "eval")
end
end
- describe "web tools" do
- it "includes fetch and web_search in read permissions" do
- tools = subject.tools_for([:read])
- names = tools.map { |t| t.dig(:function, :name) }
- expect(names).to include("fetch", "web_search")
- end
-
+ describe "aliases" do
it "resolves web and get aliases to fetch" do
expect(Elelem::Toolbox::TOOL_ALIASES["web"]).to eq("fetch")
expect(Elelem::Toolbox::TOOL_ALIASES["get"]).to eq("fetch")
@@ -64,26 +19,25 @@ RSpec.describe Elelem::Toolbox do
it "resolves duckduckgo alias to web_search" do
expect(Elelem::Toolbox::TOOL_ALIASES["duckduckgo"]).to eq("web_search")
end
- end
- describe "#run_tool mode enforcement" do
- it "allows tool execution when mode matches" do
- result = subject.run_tool("read", { "path" => __FILE__ }, permissions: [:read])
- expect(result[:content]).to include("RSpec.describe")
+ it "resolves bash alias to exec" do
+ expect(Elelem::Toolbox::TOOL_ALIASES["bash"]).to eq("exec")
end
+ end
- it "blocks tool execution when mode does not match" do
- result = subject.run_tool("exec", { "cmd" => "echo hello" }, permissions: [:read])
- expect(result[:error]).to include("not available in current mode")
+ describe "#run_tool" do
+ it "executes tools" do
+ result = subject.run_tool("read", { "path" => __FILE__ })
+ expect(result[:content]).to include("RSpec.describe")
end
- it "resolves aliases and enforces mode" do
- result = subject.run_tool("bash", { "cmd" => "echo hello" }, permissions: [:read])
- expect(result[:error]).to include("not available in current mode")
+ it "resolves aliases" do
+ result = subject.run_tool("open", { "path" => __FILE__ })
+ expect(result[:content]).to include("RSpec.describe")
end
it "returns unknown tool error for non-existent tools" do
- result = subject.run_tool("nonexistent", {}, permissions: [:read])
+ result = subject.run_tool("nonexistent", {})
expect(result[:error]).to include("Unknown tool")
end
end
@@ -96,9 +50,9 @@ RSpec.describe Elelem::Toolbox do
{ greeting: "Hello, " + args['name']+ "!" }
end
RUBY
- }, permissions: [:execute])
+ })
- expect(subject.tools_for(:execute)).to include(hash_including({
+ expect(subject.tools).to include(hash_including({
type: "function",
function: {
name: "hello",
@@ -119,25 +73,25 @@ RSpec.describe Elelem::Toolbox do
{ sum: args["a"] + args["b"] }
end
RUBY
- }, permissions: [:execute])
+ })
- result = subject.run_tool("add", { "a" => 5, "b" => 3 }, permissions: [:execute])
+ result = subject.run_tool("add", { "a" => 5, "b" => 3 })
expect(result[:sum]).to eq(8)
end
it "allows LLM to inspect tool schemas" do
- result = subject.run_tool("eval", { "ruby" => "tool_schema('read')" }, permissions: [:execute])
+ result = subject.run_tool("eval", { "ruby" => "tool_schema('read')" })
expect(result[:result]).to be_a(Hash)
expect(result[:result].dig(:function, :name)).to eq("read")
end
it "executes arbitrary Ruby code" do
- result = subject.run_tool("eval", { "ruby" => "2 + 2" }, permissions: [:execute])
+ result = subject.run_tool("eval", { "ruby" => "2 + 2" })
expect(result[:result]).to eq(4)
end
it "handles errors gracefully" do
- result = subject.run_tool("eval", { "ruby" => "undefined_variable" }, permissions: [:execute])
+ result = subject.run_tool("eval", { "ruby" => "undefined_variable" })
expect(result[:error]).to include("undefined")
expect(result[:backtrace]).to be_an(Array)
end
elelem.gemspec
@@ -29,7 +29,6 @@ Gem::Specification.new do |spec|
"lib/elelem/agent.rb",
"lib/elelem/application.rb",
"lib/elelem/conversation.rb",
- "lib/elelem/git_context.rb",
"lib/elelem/system_prompt.erb",
"lib/elelem/terminal.rb",
"lib/elelem/tool.rb",