elelem 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/lib/elelem/agent.rb +13 -10
- data/lib/elelem/api.rb +2 -4
- data/lib/elelem/application.rb +23 -23
- data/lib/elelem/configuration.rb +33 -3
- data/lib/elelem/conversation.rb +18 -11
- data/lib/elelem/mcp_client.rb +136 -0
- data/lib/elelem/states/idle.rb +23 -0
- data/lib/elelem/states/working/error.rb +19 -0
- data/lib/elelem/states/working/executing.rb +19 -0
- data/lib/elelem/states/working/state.rb +26 -0
- data/lib/elelem/states/working/talking.rb +19 -0
- data/lib/elelem/states/working/thinking.rb +18 -0
- data/lib/elelem/states/working/waiting.rb +29 -0
- data/lib/elelem/states/working.rb +34 -0
- data/lib/elelem/system_prompt.erb +7 -0
- data/lib/elelem/tool.rb +32 -0
- data/lib/elelem/toolbox/bash.rb +57 -0
- data/lib/elelem/toolbox/mcp.rb +37 -0
- data/lib/elelem/tools.rb +14 -41
- data/lib/elelem/tui.rb +33 -5
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +21 -1
- metadata +85 -6
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/lib/elelem/state.rb +0 -111
- data/mise.toml +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d4a99b7addd5861f402c297ecfd19bc52e8edc6bba89422c2dec195a8cdcd13
|
4
|
+
data.tar.gz: d663221598cb4a843879ae191d6b7d8f62318be15d121803f58f752cecc79bbc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cdc7ccfb94de895a32f5be4cc310f23ca781119628fcbec4bd95a50df2c5b0eb3caa474c3a9058b6120c744e56f53001eb8c67b09cad7934f9b03292202e3c88
|
7
|
+
data.tar.gz: 9732e722f6d1c040d4c497b6826bd53ac425ad129befdd37da0e7ecd60b9bf4e6c96464a44f89a4cd118ca79b5b675ac6190ebf157c21bf4d06aa8ced13507f9
|
data/CHANGELOG.md
CHANGED
data/lib/elelem/agent.rb
CHANGED
@@ -2,14 +2,19 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Agent
|
5
|
-
attr_reader :api, :conversation, :logger
|
5
|
+
attr_reader :api, :conversation, :logger, :model, :tui
|
6
6
|
|
7
7
|
def initialize(configuration)
|
8
8
|
@api = configuration.api
|
9
|
+
@tui = configuration.tui
|
9
10
|
@configuration = configuration
|
11
|
+
@model = configuration.model
|
10
12
|
@conversation = configuration.conversation
|
11
13
|
@logger = configuration.logger
|
12
|
-
|
14
|
+
|
15
|
+
at_exit { cleanup }
|
16
|
+
|
17
|
+
transition_to(States::Idle.new)
|
13
18
|
end
|
14
19
|
|
15
20
|
def repl
|
@@ -23,14 +28,6 @@ module Elelem
|
|
23
28
|
@current_state = next_state
|
24
29
|
end
|
25
30
|
|
26
|
-
def prompt(message)
|
27
|
-
configuration.tui.prompt(message)
|
28
|
-
end
|
29
|
-
|
30
|
-
def say(message, colour: :default, newline: false)
|
31
|
-
configuration.tui.say(message, colour: colour, newline: newline)
|
32
|
-
end
|
33
|
-
|
34
31
|
def execute(tool_call)
|
35
32
|
logger.debug("Execute: #{tool_call}")
|
36
33
|
configuration.tools.execute(tool_call)
|
@@ -38,9 +35,15 @@ module Elelem
|
|
38
35
|
|
39
36
|
def quit
|
40
37
|
logger.debug("Exiting...")
|
38
|
+
cleanup
|
41
39
|
exit
|
42
40
|
end
|
43
41
|
|
42
|
+
def cleanup
|
43
|
+
logger.debug("Cleaning up agent...")
|
44
|
+
configuration.cleanup
|
45
|
+
end
|
46
|
+
|
44
47
|
private
|
45
48
|
|
46
49
|
attr_reader :configuration, :current_state
|
data/lib/elelem/api.rb
CHANGED
@@ -8,7 +8,7 @@ module Elelem
|
|
8
8
|
@configuration = configuration
|
9
9
|
end
|
10
10
|
|
11
|
-
def chat(messages)
|
11
|
+
def chat(messages, &block)
|
12
12
|
body = {
|
13
13
|
messages: messages,
|
14
14
|
model: configuration.model,
|
@@ -28,9 +28,7 @@ module Elelem
|
|
28
28
|
configuration.http.request(req) do |response|
|
29
29
|
raise response.inspect unless response.code == "200"
|
30
30
|
|
31
|
-
response.read_body
|
32
|
-
yield(chunk)
|
33
|
-
end
|
31
|
+
response.read_body(&block)
|
34
32
|
end
|
35
33
|
end
|
36
34
|
end
|
data/lib/elelem/application.rb
CHANGED
@@ -4,29 +4,29 @@ module Elelem
|
|
4
4
|
class Application < Thor
|
5
5
|
desc "chat", "Start the REPL"
|
6
6
|
method_option :help,
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
aliases: "-h",
|
8
|
+
type: :boolean,
|
9
|
+
desc: "Display usage information"
|
10
10
|
method_option :host,
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
aliases: "--host",
|
12
|
+
type: :string,
|
13
|
+
desc: "Ollama host",
|
14
|
+
default: ENV.fetch("OLLAMA_HOST", "localhost:11434")
|
15
15
|
method_option :model,
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
16
|
+
aliases: "--model",
|
17
|
+
type: :string,
|
18
|
+
desc: "Ollama model",
|
19
|
+
default: ENV.fetch("OLLAMA_MODEL", "gpt-oss")
|
20
20
|
method_option :token,
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
21
|
+
aliases: "--token",
|
22
|
+
type: :string,
|
23
|
+
desc: "Ollama token",
|
24
|
+
default: ENV.fetch("OLLAMA_API_KEY", nil)
|
25
25
|
method_option :debug,
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
aliases: "--debug",
|
27
|
+
type: :boolean,
|
28
|
+
desc: "Debug mode",
|
29
|
+
default: false
|
30
30
|
def chat(*)
|
31
31
|
if options[:help]
|
32
32
|
invoke :help, ["chat"]
|
@@ -37,17 +37,17 @@ module Elelem
|
|
37
37
|
token: options[:token],
|
38
38
|
debug: options[:debug]
|
39
39
|
)
|
40
|
-
say "
|
41
|
-
say
|
40
|
+
say "Agent (#{configuration.model})", :green
|
41
|
+
say configuration.tools.banner.to_s, :green
|
42
42
|
|
43
43
|
agent = Agent.new(configuration)
|
44
44
|
agent.repl
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
desc "version", "
|
48
|
+
desc "version", "The version of this CLI"
|
49
49
|
def version
|
50
|
-
|
50
|
+
say "v#{Elelem::VERSION}"
|
51
51
|
end
|
52
52
|
map %w[--version -v] => :version
|
53
53
|
end
|
data/lib/elelem/configuration.rb
CHANGED
@@ -28,7 +28,7 @@ module Elelem
|
|
28
28
|
|
29
29
|
def logger
|
30
30
|
@logger ||= Logger.new(debug ? "elelem.log" : "/dev/null").tap do |logger|
|
31
|
-
logger.formatter = ->(_, _, _, message) { message.strip
|
31
|
+
logger.formatter = ->(_, _, _, message) { "#{message.to_s.strip}\n" }
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
@@ -37,11 +37,22 @@ module Elelem
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def conversation
|
40
|
-
@conversation ||= Conversation.new
|
40
|
+
@conversation ||= Conversation.new.tap do |conversation|
|
41
|
+
resources = mcp_clients.map do |client|
|
42
|
+
client.resources.map do |resource|
|
43
|
+
resource["uri"]
|
44
|
+
end
|
45
|
+
end.flatten
|
46
|
+
conversation.add(role: :tool, content: resources)
|
47
|
+
end
|
41
48
|
end
|
42
49
|
|
43
50
|
def tools
|
44
|
-
@tools ||= Tools.new
|
51
|
+
@tools ||= Tools.new(self, [Toolbox::Bash.new(self)] + mcp_tools)
|
52
|
+
end
|
53
|
+
|
54
|
+
def cleanup
|
55
|
+
@mcp_clients&.each(&:shutdown)
|
45
56
|
end
|
46
57
|
|
47
58
|
private
|
@@ -49,5 +60,24 @@ module Elelem
|
|
49
60
|
def scheme
|
50
61
|
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
|
51
62
|
end
|
63
|
+
|
64
|
+
def mcp_tools
|
65
|
+
@mcp_tools ||= mcp_clients.map do |client|
|
66
|
+
client.tools.map do |tool|
|
67
|
+
Toolbox::MCP.new(client, tui, tool)
|
68
|
+
end
|
69
|
+
end.flatten
|
70
|
+
end
|
71
|
+
|
72
|
+
def mcp_clients
|
73
|
+
@mcp_clients ||= begin
|
74
|
+
config = Pathname.pwd.join(".mcp.json")
|
75
|
+
return [] unless config.exist?
|
76
|
+
|
77
|
+
JSON.parse(config.read).map do |_key, value|
|
78
|
+
MCPClient.new(self, [value["command"]] + value["args"])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
52
82
|
end
|
53
83
|
end
|
data/lib/elelem/conversation.rb
CHANGED
@@ -2,16 +2,9 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Conversation
|
5
|
-
|
6
|
-
You are ChatGPT, a helpful assistant with reasoning capabilities.
|
7
|
-
Current date: #{Time.now.strftime("%Y-%m-%d")}.
|
8
|
-
System info: `uname -a` output: #{`uname -a`.strip}
|
9
|
-
Reasoning: high
|
10
|
-
SYS
|
5
|
+
ROLES = %i[system assistant user tool].freeze
|
11
6
|
|
12
|
-
|
13
|
-
|
14
|
-
def initialize(items = [{ role: "system", content: SYSTEM_MESSAGE }])
|
7
|
+
def initialize(items = [{ role: "system", content: system_prompt }])
|
15
8
|
@items = items
|
16
9
|
end
|
17
10
|
|
@@ -23,12 +16,26 @@ module Elelem
|
|
23
16
|
def add(role: :user, content: "")
|
24
17
|
role = role.to_sym
|
25
18
|
raise "unknown role: #{role}" unless ROLES.include?(role)
|
26
|
-
return if content.empty?
|
19
|
+
return if content.nil? || content.empty?
|
27
20
|
|
28
21
|
if @items.last && @items.last[:role] == role
|
29
22
|
@items.last[:content] += content
|
30
23
|
else
|
31
|
-
@items.push({ role: role, content: content })
|
24
|
+
@items.push({ role: role, content: normalize(content) })
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def system_prompt
|
31
|
+
ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
|
32
|
+
end
|
33
|
+
|
34
|
+
def normalize(content)
|
35
|
+
if content.is_a?(Array)
|
36
|
+
content.join(", ")
|
37
|
+
else
|
38
|
+
content.to_s
|
32
39
|
end
|
33
40
|
end
|
34
41
|
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class MCPClient
|
5
|
+
attr_reader :tools, :resources
|
6
|
+
|
7
|
+
def initialize(configuration, command = [])
|
8
|
+
@configuration = configuration
|
9
|
+
@stdin, @stdout, @stderr, @worker = Open3.popen3(*command, pgroup: true)
|
10
|
+
|
11
|
+
# 1. Send initialize request
|
12
|
+
send_request(
|
13
|
+
method: "initialize",
|
14
|
+
params: {
|
15
|
+
protocolVersion: "2025-06-08",
|
16
|
+
capabilities: {
|
17
|
+
tools: {}
|
18
|
+
},
|
19
|
+
clientInfo: {
|
20
|
+
name: "Elelem",
|
21
|
+
version: Elelem::VERSION
|
22
|
+
}
|
23
|
+
}
|
24
|
+
)
|
25
|
+
|
26
|
+
# 2. Send initialized notification (optional for some MCP servers)
|
27
|
+
send_notification(method: "notifications/initialized")
|
28
|
+
|
29
|
+
# 3. Now we can request tools
|
30
|
+
@tools = send_request(method: "tools/list")&.dig("tools") || []
|
31
|
+
@resources = send_request(method: "resources/list")&.dig("resources") || []
|
32
|
+
end
|
33
|
+
|
34
|
+
def connected?
|
35
|
+
return false unless @worker&.alive?
|
36
|
+
return false unless @stdin && !@stdin.closed?
|
37
|
+
return false unless @stdout && !@stdout.closed?
|
38
|
+
|
39
|
+
begin
|
40
|
+
Process.getpgid(@worker.pid)
|
41
|
+
true
|
42
|
+
rescue Errno::ESRCH
|
43
|
+
false
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def call(name, arguments = {})
|
48
|
+
send_request(
|
49
|
+
method: "tools/call",
|
50
|
+
params: {
|
51
|
+
name: name,
|
52
|
+
arguments: arguments
|
53
|
+
}
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def shutdown
|
58
|
+
return unless connected?
|
59
|
+
|
60
|
+
configuration.logger.debug("Shutting down MCP client")
|
61
|
+
|
62
|
+
[@stdin, @stdout, @stderr].each do |stream|
|
63
|
+
stream&.close unless stream&.closed?
|
64
|
+
end
|
65
|
+
|
66
|
+
return unless @worker&.alive?
|
67
|
+
|
68
|
+
begin
|
69
|
+
Process.kill("TERM", @worker.pid)
|
70
|
+
# Give it 2 seconds to terminate gracefully
|
71
|
+
Timeout.timeout(2) { @worker.value }
|
72
|
+
rescue Timeout::Error
|
73
|
+
# Force kill if it doesn't respond
|
74
|
+
begin
|
75
|
+
Process.kill("KILL", @worker.pid)
|
76
|
+
rescue StandardError
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
rescue Errno::ESRCH
|
80
|
+
# Process already dead
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
attr_reader :stdin, :stdout, :stderr, :worker, :configuration
|
87
|
+
|
88
|
+
def send_request(method:, params: {})
|
89
|
+
return {} unless connected?
|
90
|
+
|
91
|
+
request = {
|
92
|
+
jsonrpc: "2.0",
|
93
|
+
id: Time.now.to_i,
|
94
|
+
method: method
|
95
|
+
}
|
96
|
+
request[:params] = params unless params.empty?
|
97
|
+
configuration.logger.debug(JSON.pretty_generate(request))
|
98
|
+
|
99
|
+
@stdin.puts(JSON.generate(request))
|
100
|
+
@stdin.flush
|
101
|
+
|
102
|
+
response_line = @stdout.gets&.strip
|
103
|
+
return {} if response_line.nil? || response_line.empty?
|
104
|
+
|
105
|
+
response = JSON.parse(response_line)
|
106
|
+
configuration.logger.debug(JSON.pretty_generate(response))
|
107
|
+
|
108
|
+
if response["error"]
|
109
|
+
configuration.logger.error(response["error"]["message"])
|
110
|
+
{ error: response["error"]["message"] }
|
111
|
+
else
|
112
|
+
response["result"]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def send_notification(method:, params: {})
|
117
|
+
return unless connected?
|
118
|
+
|
119
|
+
notification = {
|
120
|
+
jsonrpc: "2.0",
|
121
|
+
method: method
|
122
|
+
}
|
123
|
+
notification[:params] = params unless params.empty?
|
124
|
+
configuration.logger.debug("Sending notification: #{JSON.pretty_generate(notification)}")
|
125
|
+
@stdin.puts(JSON.generate(notification))
|
126
|
+
@stdin.flush
|
127
|
+
|
128
|
+
response_line = @stdout.gets&.strip
|
129
|
+
return {} if response_line.nil? || response_line.empty?
|
130
|
+
|
131
|
+
response = JSON.parse(response_line)
|
132
|
+
configuration.logger.debug(JSON.pretty_generate(response))
|
133
|
+
response
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
class Idle
|
6
|
+
def run(agent)
|
7
|
+
agent.logger.debug("Idling...")
|
8
|
+
agent.tui.say("#{Dir.pwd} (#{agent.model}) [#{git_branch}]", colour: :magenta, newline: true)
|
9
|
+
input = agent.tui.prompt("モ ")
|
10
|
+
agent.quit if input.nil? || input.empty? || input == "exit" || input == "quit"
|
11
|
+
|
12
|
+
agent.conversation.add(role: :user, content: input)
|
13
|
+
agent.transition_to(Working)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def git_branch
|
19
|
+
`git branch --no-color --show-current --no-abbrev`.strip
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class Error < State
|
7
|
+
def initialize(agent, error_message)
|
8
|
+
super(agent, "X", :red)
|
9
|
+
@error_message = error_message
|
10
|
+
end
|
11
|
+
|
12
|
+
def process(_message)
|
13
|
+
agent.tui.say("\nTool execution failed: #{@error_message}", colour: :red)
|
14
|
+
Waiting.new(agent)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class Executing < State
|
7
|
+
def process(message)
|
8
|
+
if message["tool_calls"]&.any?
|
9
|
+
message["tool_calls"].each do |tool_call|
|
10
|
+
agent.conversation.add(role: :tool, content: agent.execute(tool_call))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Waiting.new(agent)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class State
|
7
|
+
attr_reader :agent
|
8
|
+
|
9
|
+
def initialize(agent, icon, colour)
|
10
|
+
@agent = agent
|
11
|
+
|
12
|
+
agent.logger.debug("#{display_name}...")
|
13
|
+
agent.tui.show_progress("#{display_name}...", icon, colour: colour)
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(message)
|
17
|
+
process(message)
|
18
|
+
end
|
19
|
+
|
20
|
+
def display_name
|
21
|
+
self.class.name.split("::").last
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class Talking < State
|
7
|
+
def process(message)
|
8
|
+
if message["content"] && !message["content"]&.empty?
|
9
|
+
agent.conversation.add(role: message["role"], content: message["content"])
|
10
|
+
agent.tui.say(message["content"], colour: :default, newline: false)
|
11
|
+
self
|
12
|
+
else
|
13
|
+
Waiting.new(agent).process(message)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class Thinking < State
|
7
|
+
def process(message)
|
8
|
+
if message["thinking"] && !message["thinking"]&.empty?
|
9
|
+
agent.tui.say(message["thinking"], colour: :gray, newline: false)
|
10
|
+
self
|
11
|
+
else
|
12
|
+
Waiting.new(agent).process(message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class Waiting < State
|
7
|
+
def initialize(agent)
|
8
|
+
super(agent, ".", :cyan)
|
9
|
+
end
|
10
|
+
|
11
|
+
def process(message)
|
12
|
+
state_for(message)&.process(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def state_for(message)
|
18
|
+
if message["thinking"] && !message["thinking"].empty?
|
19
|
+
Thinking.new(agent, "*", :yellow)
|
20
|
+
elsif message["tool_calls"]&.any?
|
21
|
+
Executing.new(agent, ">", :magenta)
|
22
|
+
elsif message["content"] && !message["content"].empty?
|
23
|
+
Talking.new(agent, "~", :white)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module States
|
5
|
+
module Working
|
6
|
+
class << self
|
7
|
+
def run(agent)
|
8
|
+
done = false
|
9
|
+
state = Waiting.new(agent)
|
10
|
+
|
11
|
+
loop do
|
12
|
+
agent.api.chat(agent.conversation.history) do |chunk|
|
13
|
+
response = JSON.parse(chunk)
|
14
|
+
message = normalize(response["message"] || {})
|
15
|
+
done = response["done"]
|
16
|
+
|
17
|
+
agent.logger.debug("#{state.display_name}: #{message}")
|
18
|
+
state = state.run(message)
|
19
|
+
end
|
20
|
+
|
21
|
+
break if state.nil?
|
22
|
+
break if done && agent.conversation.history.last[:role] != :tool
|
23
|
+
end
|
24
|
+
|
25
|
+
agent.transition_to(States::Idle.new)
|
26
|
+
end
|
27
|
+
|
28
|
+
def normalize(message)
|
29
|
+
message.reject { |_key, value| value.empty? }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
**Shell Master** — bash>code; compose>write; pipe everything; /proc/sys native; automate fast; streams/transforms; POSIX+GNU; man(1) first; no cleverness.
|
2
|
+
|
3
|
+
Time: `<%= Time.now.strftime("%Y-%m-%d %H:%M:%S") %>`
|
4
|
+
Project Directory: `<%= Dir.pwd %>`
|
5
|
+
System Info: `<%= `uname -a`.strip %>`
|
6
|
+
|
7
|
+
Ready to hack.
|
data/lib/elelem/tool.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
class Tool
|
5
|
+
attr_reader :name, :description, :parameters
|
6
|
+
|
7
|
+
def initialize(name, description, parameters)
|
8
|
+
@name = name
|
9
|
+
@description = description
|
10
|
+
@parameters = parameters
|
11
|
+
end
|
12
|
+
|
13
|
+
def banner
|
14
|
+
[name, parameters].join(": ")
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?(args)
|
18
|
+
JSON::Validator.validate(parameters, args, insert_defaults: true)
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_h
|
22
|
+
{
|
23
|
+
type: "function",
|
24
|
+
function: {
|
25
|
+
name: name,
|
26
|
+
description: description,
|
27
|
+
parameters: parameters
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module Toolbox
|
5
|
+
class Bash < ::Elelem::Tool
|
6
|
+
attr_reader :tui
|
7
|
+
|
8
|
+
def initialize(configuration)
|
9
|
+
@tui = configuration.tui
|
10
|
+
super("bash", "Run commands in /bin/bash -c. Full access to filesystem, network, processes, and all Unix tools.", {
|
11
|
+
type: "object",
|
12
|
+
properties: {
|
13
|
+
command: { type: "string" }
|
14
|
+
},
|
15
|
+
required: ["command"]
|
16
|
+
})
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(args)
|
20
|
+
command = args["command"]
|
21
|
+
output_buffer = []
|
22
|
+
|
23
|
+
Open3.popen3("/bin/bash", "-c", command) do |stdin, stdout, stderr, wait_thread|
|
24
|
+
stdin.close
|
25
|
+
streams = [stdout, stderr]
|
26
|
+
|
27
|
+
until streams.empty?
|
28
|
+
ready = IO.select(streams, nil, nil, 0.1)
|
29
|
+
|
30
|
+
if ready
|
31
|
+
ready[0].each do |io|
|
32
|
+
data = io.read_nonblock(4096)
|
33
|
+
output_buffer << data
|
34
|
+
|
35
|
+
if io == stderr
|
36
|
+
tui.say(data, colour: :red, newline: false)
|
37
|
+
else
|
38
|
+
tui.say(data, newline: false)
|
39
|
+
end
|
40
|
+
rescue IO::WaitReadable
|
41
|
+
next
|
42
|
+
rescue EOFError
|
43
|
+
streams.delete(io)
|
44
|
+
end
|
45
|
+
elsif !wait_thread.alive?
|
46
|
+
break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
wait_thread.value
|
51
|
+
end
|
52
|
+
|
53
|
+
output_buffer.join
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Elelem
|
4
|
+
module Toolbox
|
5
|
+
class MCP < ::Elelem::Tool
|
6
|
+
attr_reader :client, :tui
|
7
|
+
|
8
|
+
def initialize(client, tui, tool)
|
9
|
+
@client = client
|
10
|
+
@tui = tui
|
11
|
+
super(tool["name"], tool["description"], tool["inputSchema"] || {})
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(args)
|
15
|
+
unless client.connected?
|
16
|
+
tui.say("MCP connection lost", colour: :red)
|
17
|
+
return ""
|
18
|
+
end
|
19
|
+
|
20
|
+
result = client.call(name, args)
|
21
|
+
tui.say(JSON.pretty_generate(result), newline: true)
|
22
|
+
|
23
|
+
if result.nil? || result.empty?
|
24
|
+
tui.say("Tool call failed: no response from MCP server", colour: :red)
|
25
|
+
return result
|
26
|
+
end
|
27
|
+
|
28
|
+
if result["error"]
|
29
|
+
tui.say(result["error"], colour: :red)
|
30
|
+
return result
|
31
|
+
end
|
32
|
+
|
33
|
+
result.dig("content", 0, "text") || result.to_s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/elelem/tools.rb
CHANGED
@@ -2,61 +2,34 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Tools
|
5
|
-
|
6
|
-
|
7
|
-
type: "function",
|
8
|
-
function: {
|
9
|
-
name: "execute_command",
|
10
|
-
description: "Execute a shell command.",
|
11
|
-
parameters: {
|
12
|
-
type: "object",
|
13
|
-
properties: {
|
14
|
-
command: { type: "string" },
|
15
|
-
},
|
16
|
-
required: ["command"]
|
17
|
-
}
|
18
|
-
},
|
19
|
-
handler: lambda { |args|
|
20
|
-
stdout, stderr, _status = Open3.capture3("/bin/sh", "-c", args["command"])
|
21
|
-
stdout + stderr
|
22
|
-
}
|
23
|
-
},
|
24
|
-
]
|
25
|
-
|
26
|
-
def initialize(tools = DEFAULT_TOOLS)
|
5
|
+
def initialize(configuration, tools)
|
6
|
+
@configuration = configuration
|
27
7
|
@tools = tools
|
28
8
|
end
|
29
9
|
|
30
10
|
def banner
|
31
|
-
|
32
|
-
[
|
33
|
-
h.dig(:function, :name),
|
34
|
-
h.dig(:function, :description)
|
35
|
-
].join(": ")
|
36
|
-
end.sort.join("\n ")
|
11
|
+
tools.map(&:banner).sort.join("\n ")
|
37
12
|
end
|
38
13
|
|
39
14
|
def execute(tool_call)
|
40
15
|
name = tool_call.dig("function", "name")
|
41
16
|
args = tool_call.dig("function", "arguments")
|
42
17
|
|
43
|
-
tool =
|
44
|
-
|
18
|
+
tool = tools.find { |tool| tool.name == name }
|
19
|
+
return "Invalid function name: #{name}" if tool.nil?
|
20
|
+
return "Invalid function arguments: #{args}" unless tool.valid?(args)
|
21
|
+
|
22
|
+
CLI::UI::Frame.open(name) do
|
23
|
+
tool.call(args)
|
45
24
|
end
|
46
|
-
tool&.fetch(:handler)&.call(args)
|
47
25
|
end
|
48
26
|
|
49
27
|
def to_h
|
50
|
-
|
51
|
-
{
|
52
|
-
type: tool[:type],
|
53
|
-
function: {
|
54
|
-
name: tool.dig(:function, :name),
|
55
|
-
description: tool.dig(:function, :description),
|
56
|
-
parameters: tool.dig(:function, :parameters)
|
57
|
-
}
|
58
|
-
}
|
59
|
-
end
|
28
|
+
tools.map(&:to_h)
|
60
29
|
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :configuration, :tools
|
61
34
|
end
|
62
35
|
end
|
data/lib/elelem/tui.rb
CHANGED
@@ -10,24 +10,52 @@ module Elelem
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def prompt(message)
|
13
|
-
|
14
|
-
stdin.gets&.chomp
|
13
|
+
Reline.readline(message, true)
|
15
14
|
end
|
16
15
|
|
17
16
|
def say(message, colour: :default, newline: false)
|
18
|
-
formatted_message = colourize(message, colour: colour)
|
19
17
|
if newline
|
20
|
-
stdout.puts(
|
18
|
+
stdout.puts(colourize(message, colour: colour))
|
21
19
|
else
|
22
|
-
stdout.print(
|
20
|
+
stdout.print(colourize(message, colour: colour))
|
23
21
|
end
|
24
22
|
stdout.flush
|
25
23
|
end
|
26
24
|
|
25
|
+
def show_progress(message, icon = ".", colour: :gray)
|
26
|
+
timestamp = Time.now.strftime("%H:%M:%S")
|
27
|
+
say("\n[#{icon}] #{timestamp} #{message}", colour: colour, newline: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def clear_line
|
31
|
+
say("\r#{" " * 80}\r", newline: false)
|
32
|
+
end
|
33
|
+
|
34
|
+
def complete_progress(message = "Completed")
|
35
|
+
clear_line
|
36
|
+
show_progress(message, "✓", colour: :green)
|
37
|
+
end
|
38
|
+
|
27
39
|
private
|
28
40
|
|
29
41
|
def colourize(text, colour: :default)
|
30
42
|
case colour
|
43
|
+
when :black
|
44
|
+
"\e[30m#{text}\e[0m"
|
45
|
+
when :red
|
46
|
+
"\e[31m#{text}\e[0m"
|
47
|
+
when :green
|
48
|
+
"\e[32m#{text}\e[0m"
|
49
|
+
when :yellow
|
50
|
+
"\e[33m#{text}\e[0m"
|
51
|
+
when :blue
|
52
|
+
"\e[34m#{text}\e[0m"
|
53
|
+
when :magenta
|
54
|
+
"\e[35m#{text}\e[0m"
|
55
|
+
when :cyan
|
56
|
+
"\e[36m#{text}\e[0m"
|
57
|
+
when :white
|
58
|
+
"\e[37m#{text}\e[0m"
|
31
59
|
when :gray
|
32
60
|
"\e[90m#{text}\e[0m"
|
33
61
|
else
|
data/lib/elelem/version.rb
CHANGED
data/lib/elelem.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "cli/ui"
|
4
|
+
require "erb"
|
3
5
|
require "json"
|
6
|
+
require "json-schema"
|
4
7
|
require "logger"
|
5
8
|
require "net/http"
|
6
9
|
require "open3"
|
10
|
+
require "reline"
|
7
11
|
require "thor"
|
12
|
+
require "timeout"
|
8
13
|
require "uri"
|
9
14
|
|
10
15
|
require_relative "elelem/agent"
|
@@ -12,11 +17,26 @@ require_relative "elelem/api"
|
|
12
17
|
require_relative "elelem/application"
|
13
18
|
require_relative "elelem/configuration"
|
14
19
|
require_relative "elelem/conversation"
|
15
|
-
require_relative "elelem/
|
20
|
+
require_relative "elelem/mcp_client"
|
21
|
+
require_relative "elelem/states/idle"
|
22
|
+
require_relative "elelem/states/working"
|
23
|
+
require_relative "elelem/states/working/state"
|
24
|
+
require_relative "elelem/states/working/error"
|
25
|
+
require_relative "elelem/states/working/executing"
|
26
|
+
require_relative "elelem/states/working/talking"
|
27
|
+
require_relative "elelem/states/working/thinking"
|
28
|
+
require_relative "elelem/states/working/waiting"
|
29
|
+
require_relative "elelem/tool"
|
30
|
+
require_relative "elelem/toolbox/bash"
|
31
|
+
require_relative "elelem/toolbox/mcp"
|
16
32
|
require_relative "elelem/tools"
|
17
33
|
require_relative "elelem/tui"
|
18
34
|
require_relative "elelem/version"
|
19
35
|
|
36
|
+
CLI::UI::StdoutRouter.enable
|
37
|
+
Reline.input = $stdin
|
38
|
+
Reline.output = $stdout
|
39
|
+
|
20
40
|
module Elelem
|
21
41
|
class Error < StandardError; end
|
22
42
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: elelem
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- mo khan
|
@@ -9,6 +9,34 @@ bindir: exe
|
|
9
9
|
cert_chain: []
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: cli-ui
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: erb
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
12
40
|
- !ruby/object:Gem::Dependency
|
13
41
|
name: json
|
14
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -23,6 +51,20 @@ dependencies:
|
|
23
51
|
- - ">="
|
24
52
|
- !ruby/object:Gem::Version
|
25
53
|
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: json-schema
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
26
68
|
- !ruby/object:Gem::Dependency
|
27
69
|
name: logger
|
28
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -65,6 +107,20 @@ dependencies:
|
|
65
107
|
- - ">="
|
66
108
|
- !ruby/object:Gem::Version
|
67
109
|
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: reline
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
68
124
|
- !ruby/object:Gem::Dependency
|
69
125
|
name: thor
|
70
126
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,6 +135,20 @@ dependencies:
|
|
79
135
|
- - ">="
|
80
136
|
- !ruby/object:Gem::Version
|
81
137
|
version: '0'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: timeout
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :runtime
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
82
152
|
- !ruby/object:Gem::Dependency
|
83
153
|
name: uri
|
84
154
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,8 +171,6 @@ executables:
|
|
101
171
|
extensions: []
|
102
172
|
extra_rdoc_files: []
|
103
173
|
files:
|
104
|
-
- ".rspec"
|
105
|
-
- ".rubocop.yml"
|
106
174
|
- CHANGELOG.md
|
107
175
|
- LICENSE.txt
|
108
176
|
- README.md
|
@@ -114,11 +182,22 @@ files:
|
|
114
182
|
- lib/elelem/application.rb
|
115
183
|
- lib/elelem/configuration.rb
|
116
184
|
- lib/elelem/conversation.rb
|
117
|
-
- lib/elelem/
|
185
|
+
- lib/elelem/mcp_client.rb
|
186
|
+
- lib/elelem/states/idle.rb
|
187
|
+
- lib/elelem/states/working.rb
|
188
|
+
- lib/elelem/states/working/error.rb
|
189
|
+
- lib/elelem/states/working/executing.rb
|
190
|
+
- lib/elelem/states/working/state.rb
|
191
|
+
- lib/elelem/states/working/talking.rb
|
192
|
+
- lib/elelem/states/working/thinking.rb
|
193
|
+
- lib/elelem/states/working/waiting.rb
|
194
|
+
- lib/elelem/system_prompt.erb
|
195
|
+
- lib/elelem/tool.rb
|
196
|
+
- lib/elelem/toolbox/bash.rb
|
197
|
+
- lib/elelem/toolbox/mcp.rb
|
118
198
|
- lib/elelem/tools.rb
|
119
199
|
- lib/elelem/tui.rb
|
120
200
|
- lib/elelem/version.rb
|
121
|
-
- mise.toml
|
122
201
|
- sig/elelem.rbs
|
123
202
|
homepage: https://www.mokhan.ca
|
124
203
|
licenses:
|
@@ -135,7 +214,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
135
214
|
requirements:
|
136
215
|
- - ">="
|
137
216
|
- !ruby/object:Gem::Version
|
138
|
-
version: 3.
|
217
|
+
version: 3.4.0
|
139
218
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
140
219
|
requirements:
|
141
220
|
- - ">="
|
data/.rspec
DELETED
data/.rubocop.yml
DELETED
data/lib/elelem/state.rb
DELETED
@@ -1,111 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Elelem
|
4
|
-
class Idle
|
5
|
-
def run(agent)
|
6
|
-
agent.logger.debug("Idling...")
|
7
|
-
input = agent.prompt("\n> ")
|
8
|
-
agent.quit if input.nil? || input.empty? || input == "exit"
|
9
|
-
|
10
|
-
agent.conversation.add(role: :user, content: input)
|
11
|
-
agent.transition_to(Working.new)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
class Working
|
16
|
-
class State
|
17
|
-
attr_reader :agent
|
18
|
-
|
19
|
-
def initialize(agent)
|
20
|
-
@agent = agent
|
21
|
-
end
|
22
|
-
|
23
|
-
def display_name
|
24
|
-
self.class.name.split("::").last
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
class Waiting < State
|
29
|
-
def process(message)
|
30
|
-
state = self
|
31
|
-
|
32
|
-
if message["thinking"] && !message["thinking"].empty?
|
33
|
-
state = Thinking.new(agent)
|
34
|
-
elsif message["tool_calls"]&.any?
|
35
|
-
state = Executing.new(agent)
|
36
|
-
elsif message["content"] && !message["content"].empty?
|
37
|
-
state = Talking.new(agent)
|
38
|
-
else
|
39
|
-
state = nil
|
40
|
-
end
|
41
|
-
|
42
|
-
state&.process(message)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class Thinking < State
|
47
|
-
def process(message)
|
48
|
-
if message["thinking"] && !message["thinking"]&.empty?
|
49
|
-
agent.say(message["thinking"], colour: :gray, newline: false)
|
50
|
-
self
|
51
|
-
else
|
52
|
-
agent.say("", newline: true)
|
53
|
-
Waiting.new(agent).process(message)
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
class Executing < State
|
59
|
-
def process(message)
|
60
|
-
if message["tool_calls"]&.any?
|
61
|
-
message["tool_calls"].each do |tool_call|
|
62
|
-
agent.conversation.add(role: :tool, content: agent.execute(tool_call))
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
Waiting.new(agent)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
class Talking < State
|
71
|
-
def process(message)
|
72
|
-
if message["content"] && !message["content"]&.empty?
|
73
|
-
agent.conversation.add(role: message["role"], content: message["content"])
|
74
|
-
agent.say(message["content"], colour: :default, newline: false)
|
75
|
-
self
|
76
|
-
else
|
77
|
-
agent.say("", newline: true)
|
78
|
-
Waiting.new(agent).process(message)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def run(agent)
|
84
|
-
agent.logger.debug("Working...")
|
85
|
-
state = Waiting.new(agent)
|
86
|
-
done = false
|
87
|
-
|
88
|
-
loop do
|
89
|
-
agent.api.chat(agent.conversation.history) do |chunk|
|
90
|
-
response = JSON.parse(chunk)
|
91
|
-
message = normalize(response["message"] || {})
|
92
|
-
done = response["done"]
|
93
|
-
|
94
|
-
agent.logger.debug("#{state.display_name}: #{message}")
|
95
|
-
state = state.process(message)
|
96
|
-
end
|
97
|
-
|
98
|
-
break if state.nil?
|
99
|
-
break if done && agent.conversation.history.last[:role] != :tool
|
100
|
-
end
|
101
|
-
|
102
|
-
agent.transition_to(Idle.new)
|
103
|
-
end
|
104
|
-
|
105
|
-
private
|
106
|
-
|
107
|
-
def normalize(message)
|
108
|
-
message.reject { |_key, value| value.empty? }
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
data/mise.toml
DELETED