Commit a954505
Changed files (10)
lib
elelem
states
lib/elelem/states/working.rb
@@ -8,18 +8,38 @@ module Elelem
state = Waiting.new(agent)
loop do
- done = false
+ streaming_done = false
+ finish_reason = nil
+
agent.api.chat(agent.conversation.history) do |message|
if message["done"]
- done = true
+ streaming_done = true
next
end
- agent.logger.debug("#{state.display_name}: #{message}")
- state = state.run(message)
+ if message["finish_reason"]
+ finish_reason = message["finish_reason"]
+ agent.logger.debug("Working: finish_reason = #{finish_reason}")
+ end
+
+ new_state = state.run(message)
+ if new_state.class != state.class
+ agent.logger.info("STATE: #{state.display_name} -> #{new_state.display_name}")
+ end
+ state = new_state
end
- break if state.nil? || done
+ # Only exit when task is actually complete, not just streaming done
+ if finish_reason == "stop"
+ agent.logger.debug("Working: Task complete, exiting to Idle")
+ break
+ elsif finish_reason == "tool_calls"
+ agent.logger.debug("Working: Tool calls finished, continuing conversation")
+ # Continue loop to process tool results
+ elsif streaming_done && finish_reason.nil?
+ agent.logger.debug("Working: Streaming done but no finish_reason, continuing")
+ # Continue for cases where finish_reason comes in separate chunk
+ end
end
agent.transition_to(States::Idle.new)
lib/elelem/toolbox/file.rb
@@ -2,47 +2,62 @@
module Elelem
module Toolbox
- class File < ::Elelem::Tool
- attr_reader :tui
-
+ class File < Tool
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"
+ super(
+ "file",
+ "Read and write files",
+ {
+ type: :object,
+ properties: {
+ action: {
+ type: :string,
+ enum: ["read", "write"],
+ description: "Action to perform: read or write"
+ },
+ path: {
+ type: :string,
+ description: "File path"
+ },
+ content: {
+ type: :string,
+ description: "Content to write (only for write action)"
+ }
},
- content: {
- type: "string",
- description: "Content to write/apppend (only for write/append actions)"
- }
- },
- required: ["action", "path"]
- })
+ required: [:action, :path]
+ }
+ )
end
def call(args)
- path = Pathname.pwd.join(args["path"])
- case args["action"]
+ action = args["action"]
+ path = args["path"]
+ content = args["content"]
+
+ case action
when "read"
- path.read
+ read_file(path)
when "write"
- path.write(args["content"])
- "File written successfully"
- when "append"
- path.open("a") { |f| f << args["content"] }
- "Content appended successfully"
+ write_file(path, content)
+ else
+ "Invalid action: #{action}"
end
+ end
+
+ private
+
+ def read_file(path)
+ ::File.read(path)
+ rescue => e
+ "Error reading file: #{e.message}"
+ end
+
+ def write_file(path, content)
+ ::File.write(path, content)
+ "File written successfully"
rescue => e
- e.message
+ "Error writing file: #{e.message}"
end
end
end
-end
+end
\ No newline at end of file
lib/elelem/toolbox/git.rb
@@ -1,40 +0,0 @@
-# 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/search.rb
@@ -1,45 +0,0 @@
-# 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"] || "."
- pattern = args["pattern"]
-
- # Build search command - use git grep if in git repo, otherwise regular grep
- if ::File.directory?(".git") || `git rev-parse --git-dir 2>/dev/null`.strip != ""
- # Limit results: -m 3 = max 3 matches per file, head -20 = max 20 total lines
- command = "git grep -n -m 3 '#{pattern}'"
- command += " -- #{path}" unless path == "."
- command += " | head -20"
- else
- # For regular grep, also limit results
- command = "grep -rnw '#{pattern}' #{path} | head -20"
- end
-
- # Delegate to exec tool for consistent logging and streaming
- exec_tool = Elelem::Toolbox::Exec.new(@configuration)
- exec_tool.call({ "command" => command })
- end
- end
- end
-end
lib/elelem/agent.rb
@@ -25,23 +25,28 @@ module Elelem
end
def transition_to(next_state)
- logger.debug("Transition to: #{next_state.class.name}")
+ if @current_state
+ logger.info("AGENT: #{@current_state.class.name.split('::').last} -> #{next_state.class.name.split('::').last}")
+ else
+ logger.info("AGENT: Starting in #{next_state.class.name.split('::').last}")
+ end
@current_state = next_state
end
def execute(tool_call)
- logger.debug("Execute: #{tool_call}")
- configuration.tools.execute(tool_call)
+ tool_name = tool_call.dig("function", "name")
+ logger.debug("TOOL: Full call - #{tool_call}")
+ result = configuration.tools.execute(tool_call)
+ logger.debug("TOOL: Result (#{result.length} chars)") if result
+ result
end
def quit
- logger.debug("Exiting...")
cleanup
exit
end
def cleanup
- logger.debug("Cleaning up agent...")
configuration.cleanup
end
lib/elelem/api.rb
@@ -13,13 +13,14 @@ module Elelem
Net::HTTP.start(uri.hostname, uri.port, http_options) do |http|
http.request(build_request(messages)) do |response|
if !response.is_a?(Net::HTTPSuccess)
- configuration.logger.error(response.inspect)
+ configuration.logger.error("API: HTTP error - #{response.code} #{response.message}")
raise response.inspect
end
buffer = ""
+ chunk_count = 0
+
response.read_body do |chunk|
- configuration.logger.debug(chunk)
buffer += chunk
while (message = extract_sse_message(buffer))
@@ -30,9 +31,18 @@ module Elelem
break
end
- configuration.logger.debug(message)
json = JSON.parse(message)
- block.call(normalize(json.dig("choices", 0, "delta")))
+ delta = json.dig("choices", 0, "delta")
+ finish_reason = json.dig("choices", 0, "finish_reason")
+
+ if finish_reason
+ block.call({ "finish_reason" => finish_reason })
+ end
+
+ if delta
+ chunk_count += 1
+ block.call(normalize(delta))
+ end
end
end
end
@@ -103,7 +113,7 @@ module Elelem
temperature: 0.1,
tools: configuration.tools.to_h
}.tap do |payload|
- configuration.logger.debug(JSON.pretty_generate(payload))
+ configuration.logger.debug(JSON.pretty_generate(payload)) if configuration.debug
end
end
end
lib/elelem/configuration.rb
@@ -26,7 +26,10 @@ module Elelem
else
logger.level = ENV.fetch("LOG_LEVEL", "warn")
end
- logger.formatter = ->(_, _, _, message) { "#{message.to_s.strip}\n" }
+ logger.formatter = ->(severity, datetime, progname, message) {
+ timestamp = datetime.strftime("%H:%M:%S.%3N")
+ "[#{timestamp}] #{severity.ljust(5)} #{message.to_s.strip}\n"
+ }
end
end
@@ -45,10 +48,8 @@ module Elelem
@tools ||= Tools.new(self,
[
Toolbox::Exec.new(self),
- # Toolbox::File.new(self),
- # Toolbox::Git.new(self),
- # Toolbox::Prompt.new(self),
- Toolbox::Search.new(self),
+ Toolbox::File.new(self),
+ Toolbox::Prompt.new(self),
] + mcp_tools
)
end
lib/elelem/toolbox.rb
@@ -2,7 +2,5 @@
require_relative "toolbox/exec"
require_relative "toolbox/file"
-require_relative "toolbox/git"
require_relative "toolbox/mcp"
require_relative "toolbox/prompt"
-require_relative "toolbox/search"
lib/elelem/tools.rb
@@ -19,7 +19,6 @@ module Elelem
return "Invalid function arguments: #{args}" unless tool.valid?(args)
CLI::UI::Frame.open(name) do
- configuration.logger.debug("Calling tool: #{tool} with #{args}")
tool.call(args)
end
end
elelem.gemspec
@@ -53,10 +53,8 @@ Gem::Specification.new do |spec|
"lib/elelem/toolbox.rb",
"lib/elelem/toolbox/exec.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",