elelem 0.1.2 → 0.2.0
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 +24 -0
- data/README.md +0 -94
- data/exe/elelem +0 -3
- data/lib/elelem/agent.rb +22 -26
- data/lib/elelem/api.rb +34 -21
- data/lib/elelem/application.rb +2 -2
- data/lib/elelem/configuration.rb +45 -33
- data/lib/elelem/conversation.rb +9 -1
- data/lib/elelem/mcp_client.rb +40 -3
- 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 +55 -0
- data/lib/elelem/system_prompt.erb +16 -5
- data/lib/elelem/tool.rb +0 -84
- data/lib/elelem/toolbox/exec.rb +61 -0
- data/lib/elelem/toolbox/file.rb +66 -0
- data/lib/elelem/toolbox/mcp.rb +37 -0
- data/lib/elelem/toolbox/memory.rb +164 -0
- data/lib/elelem/toolbox/prompt.rb +25 -0
- data/lib/elelem/toolbox/web.rb +126 -0
- data/lib/elelem/toolbox.rb +8 -0
- data/lib/elelem/tools.rb +11 -3
- data/lib/elelem/tui.rb +7 -18
- data/lib/elelem/version.rb +1 -1
- data/lib/elelem.rb +16 -3
- metadata +33 -6
- data/lib/elelem/state.rb +0 -162
- data/sig/elelem.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9eecf050fb965aa627ff0266d46d6085a4cfbb52224226d1375d1ee4b41c9d2
|
4
|
+
data.tar.gz: 151e6eb08baf9f1f7ee622e25048eaf33e0ff1baa9d311d504ebe370a991af97
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d23eec1c298290ddd9480093b8ed908fbcbe1f55bb468618abaf445a6c1545fb4e43c995e8d4fce1655c7e92d255aed8237cc46fa48675e462fcb68718aa85c
|
7
|
+
data.tar.gz: f3dce29e2aa754be66f2c4cb01093f5ebb10788e96299e5925fbae59df7196cc3eed548aa154876184471ed3a4491260f969fe0409581ef8bbd89624cafedf9f
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.0] - 2025-10-15
|
4
|
+
|
5
|
+
### Added
|
6
|
+
- New `llm-ollama` executable - minimal coding agent with streaming support for Ollama
|
7
|
+
- New `llm-openai` executable - minimal coding agent for OpenAI/compatible APIs
|
8
|
+
- Memory feature for persistent context storage and retrieval
|
9
|
+
- Web fetch tool for retrieving and analyzing web content
|
10
|
+
- Streaming responses with real-time token display
|
11
|
+
- Visual "thinking" progress indicators with dots during reasoning phase
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
- **BREAKING**: Migrated from custom Net::HTTP implementation to `net-llm` gem
|
15
|
+
- API client now uses `Net::Llm::Ollama` for better reliability and maintainability
|
16
|
+
- Removed direct dependencies on `net-http` and `uri` (now transitive through net-llm)
|
17
|
+
- Maps Ollama's `thinking` field to internal `reasoning` field
|
18
|
+
- Maps Ollama's `done_reason` to internal `finish_reason`
|
19
|
+
- Improved system prompt for better agent behavior
|
20
|
+
- Enhanced error handling and logging
|
21
|
+
|
22
|
+
### Fixed
|
23
|
+
- Response processing for Ollama's native message format
|
24
|
+
- Tool argument parsing to handle both string and object formats
|
25
|
+
- Safe navigation operator usage to prevent nil errors
|
26
|
+
|
3
27
|
## [0.1.2] - 2025-08-14
|
4
28
|
|
5
29
|
### Fixed
|
data/README.md
CHANGED
@@ -58,100 +58,6 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
58
58
|
|
59
59
|
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).
|
60
60
|
|
61
|
-
REPL State Diagram
|
62
|
-
|
63
|
-
```
|
64
|
-
┌─────────────────┐
|
65
|
-
│ START/INIT │
|
66
|
-
└─────────┬───────┘
|
67
|
-
│
|
68
|
-
v
|
69
|
-
┌─────────────────┐
|
70
|
-
┌────▶│ IDLE (Prompt) │◄────┐
|
71
|
-
│ │ Shows "> " │ │
|
72
|
-
│ └─────────┬───────┘ │
|
73
|
-
│ │ │
|
74
|
-
│ │ User input │
|
75
|
-
│ v │
|
76
|
-
│ ┌─────────────────┐ │
|
77
|
-
│ │ PROCESSING │ │
|
78
|
-
│ │ INPUT │ │
|
79
|
-
│ └─────────┬───────┘ │
|
80
|
-
│ │ │
|
81
|
-
│ │ API call │
|
82
|
-
│ v │
|
83
|
-
│ ┌─────────────────┐ │
|
84
|
-
│ │ STREAMING │ │
|
85
|
-
│ ┌──▶│ RESPONSE │─────┤
|
86
|
-
│ │ └─────────┬───────┘ │
|
87
|
-
│ │ │ │ done=true
|
88
|
-
│ │ │ Parse chunk │
|
89
|
-
│ │ v │
|
90
|
-
│ │ ┌─────────────────┐ │
|
91
|
-
│ │ │ MESSAGE TYPE │ │
|
92
|
-
│ │ │ ROUTING │ │
|
93
|
-
│ │ └─────┬─┬─┬───────┘ │
|
94
|
-
│ │ │ │ │ │
|
95
|
-
┌────────┴─┴─────────┘ │ └─────────────┴──────────┐
|
96
|
-
│ │ │
|
97
|
-
v v v
|
98
|
-
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
99
|
-
│ THINKING │ │ TOOL │ │ CONTENT │
|
100
|
-
│ STATE │ │ EXECUTION │ │ OUTPUT │
|
101
|
-
│ │ │ STATE │ │ STATE │
|
102
|
-
└─────────────┘ └─────┬───────┘ └─────────────┘
|
103
|
-
│ │ │
|
104
|
-
│ │ done=false │
|
105
|
-
└───────────────────┼──────────────────────────┘
|
106
|
-
│
|
107
|
-
v
|
108
|
-
┌─────────────────┐
|
109
|
-
│ CONTINUE │
|
110
|
-
│ STREAMING │
|
111
|
-
└─────────────────┘
|
112
|
-
│
|
113
|
-
└─────────────────┐
|
114
|
-
│
|
115
|
-
┌─────────────────┐ │
|
116
|
-
│ ERROR STATE │ │
|
117
|
-
│ (Exception) │ │
|
118
|
-
└─────────────────┘ │
|
119
|
-
▲ │
|
120
|
-
│ Invalid response │
|
121
|
-
└────────────────────────────┘
|
122
|
-
|
123
|
-
EXIT CONDITIONS:
|
124
|
-
┌─────────────────────────┐
|
125
|
-
│ • User enters "" │
|
126
|
-
│ • User enters "exit" │
|
127
|
-
│ • EOF (Ctrl+D) │
|
128
|
-
│ • nil input │
|
129
|
-
└─────────────────────────┘
|
130
|
-
│
|
131
|
-
v
|
132
|
-
┌─────────────────────────┐
|
133
|
-
│ TERMINATE │
|
134
|
-
└─────────────────────────┘
|
135
|
-
```
|
136
|
-
|
137
|
-
Key Transitions:
|
138
|
-
|
139
|
-
1. IDLE → PROCESSING: User enters any non-empty, non-"exit" input
|
140
|
-
2. PROCESSING → STREAMING: API call initiated to Ollama
|
141
|
-
3. STREAMING → MESSAGE ROUTING: Each chunk received is parsed
|
142
|
-
4. MESSAGE ROUTING → States: Based on message content:
|
143
|
-
- thinking → THINKING STATE
|
144
|
-
- tool_calls → TOOL EXECUTION STATE
|
145
|
-
- content → CONTENT OUTPUT STATE
|
146
|
-
- Invalid format → ERROR STATE
|
147
|
-
5. All States → IDLE: When done=true from API response
|
148
|
-
6. TOOL EXECUTION → STREAMING: Sets done=false to continue conversation
|
149
|
-
7. Any State → TERMINATE: On exit conditions
|
150
|
-
|
151
|
-
The REPL operates as a continuous loop where the primary flow is IDLE → PROCESSING → STREAMING →
|
152
|
-
back to IDLE, with the streaming phase potentially cycling through multiple message types before
|
153
|
-
completion.
|
154
|
-
|
155
61
|
## Contributing
|
156
62
|
|
157
63
|
Bug reports and pull requests are welcome on GitHub at https://github.com/xlgmokha/elelem.
|
data/exe/elelem
CHANGED
data/lib/elelem/agent.rb
CHANGED
@@ -2,58 +2,54 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class Agent
|
5
|
-
attr_reader :api, :conversation, :logger, :model
|
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
|
10
11
|
@model = configuration.model
|
11
12
|
@conversation = configuration.conversation
|
12
13
|
@logger = configuration.logger
|
13
|
-
|
14
|
+
|
15
|
+
at_exit { cleanup }
|
16
|
+
|
17
|
+
transition_to(States::Idle.new)
|
14
18
|
end
|
15
19
|
|
16
20
|
def repl
|
17
21
|
loop do
|
18
22
|
current_state.run(self)
|
23
|
+
sleep 0.1
|
19
24
|
end
|
20
25
|
end
|
21
26
|
|
22
27
|
def transition_to(next_state)
|
23
|
-
|
28
|
+
if @current_state
|
29
|
+
logger.info("AGENT: #{@current_state.class.name.split('::').last} -> #{next_state.class.name.split('::').last}")
|
30
|
+
else
|
31
|
+
logger.info("AGENT: Starting in #{next_state.class.name.split('::').last}")
|
32
|
+
end
|
24
33
|
@current_state = next_state
|
25
34
|
end
|
26
35
|
|
27
|
-
def prompt(message)
|
28
|
-
configuration.tui.prompt(message)
|
29
|
-
end
|
30
|
-
|
31
|
-
def say(message, colour: :default, newline: false)
|
32
|
-
configuration.tui.say(message, colour: colour, newline: newline)
|
33
|
-
end
|
34
|
-
|
35
36
|
def execute(tool_call)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
configuration.tui.show_progress(message, prefix, colour: colour)
|
42
|
-
end
|
43
|
-
|
44
|
-
def clear_line
|
45
|
-
configuration.tui.clear_line
|
46
|
-
end
|
47
|
-
|
48
|
-
def complete_progress(message = "Completed")
|
49
|
-
configuration.tui.complete_progress(message)
|
37
|
+
tool_name = tool_call.dig("function", "name")
|
38
|
+
logger.debug("TOOL: Full call - #{tool_call}")
|
39
|
+
result = configuration.tools.execute(tool_call)
|
40
|
+
logger.debug("TOOL: Result (#{result.length} chars)") if result
|
41
|
+
result
|
50
42
|
end
|
51
43
|
|
52
44
|
def quit
|
53
|
-
|
45
|
+
cleanup
|
54
46
|
exit
|
55
47
|
end
|
56
48
|
|
49
|
+
def cleanup
|
50
|
+
configuration.cleanup
|
51
|
+
end
|
52
|
+
|
57
53
|
private
|
58
54
|
|
59
55
|
attr_reader :configuration, :current_state
|
data/lib/elelem/api.rb
CHANGED
@@ -1,35 +1,48 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "net/llm"
|
4
|
+
|
3
5
|
module Elelem
|
4
6
|
class Api
|
5
|
-
attr_reader :configuration
|
7
|
+
attr_reader :configuration, :client
|
6
8
|
|
7
9
|
def initialize(configuration)
|
8
10
|
@configuration = configuration
|
11
|
+
@client = Net::Llm::Ollama.new(
|
12
|
+
host: configuration.host,
|
13
|
+
model: configuration.model
|
14
|
+
)
|
9
15
|
end
|
10
16
|
|
11
17
|
def chat(messages, &block)
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
keep_alive: "5m",
|
17
|
-
options: { temperature: 0.1 },
|
18
|
-
tools: configuration.tools.to_h
|
19
|
-
}
|
20
|
-
configuration.logger.debug(JSON.pretty_generate(body))
|
21
|
-
json_body = body.to_json
|
22
|
-
|
23
|
-
req = Net::HTTP::Post.new(configuration.uri)
|
24
|
-
req["Content-Type"] = "application/json"
|
25
|
-
req.body = json_body
|
26
|
-
req["Authorization"] = "Bearer #{configuration.token}" if configuration.token
|
27
|
-
|
28
|
-
configuration.http.request(req) do |response|
|
29
|
-
raise response.inspect unless response.code == "200"
|
30
|
-
|
31
|
-
response.read_body(&block)
|
18
|
+
tools = configuration.tools.to_h
|
19
|
+
client.chat(messages, tools) do |chunk|
|
20
|
+
normalized = normalize_ollama_response(chunk)
|
21
|
+
block.call(normalized) if normalized
|
32
22
|
end
|
33
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def normalize_ollama_response(chunk)
|
28
|
+
return done_response(chunk) if chunk["done"]
|
29
|
+
|
30
|
+
normalize_message(chunk["message"])
|
31
|
+
end
|
32
|
+
|
33
|
+
def done_response(chunk)
|
34
|
+
{ "done" => true, "finish_reason" => chunk["done_reason"] || "stop" }
|
35
|
+
end
|
36
|
+
|
37
|
+
def normalize_message(message)
|
38
|
+
return nil unless message
|
39
|
+
|
40
|
+
{}.tap do |result|
|
41
|
+
result["role"] = message["role"] if message["role"]
|
42
|
+
result["content"] = message["content"] if message["content"]
|
43
|
+
result["reasoning"] = message["thinking"] if message["thinking"]
|
44
|
+
result["tool_calls"] = message["tool_calls"] if message["tool_calls"]
|
45
|
+
end.then { |r| r.empty? ? nil : r }
|
46
|
+
end
|
34
47
|
end
|
35
48
|
end
|
data/lib/elelem/application.rb
CHANGED
data/lib/elelem/configuration.rb
CHANGED
@@ -11,13 +11,6 @@ module Elelem
|
|
11
11
|
@debug = debug
|
12
12
|
end
|
13
13
|
|
14
|
-
def http
|
15
|
-
@http ||= Net::HTTP.new(uri.host, uri.port).tap do |h|
|
16
|
-
h.read_timeout = 3_600
|
17
|
-
h.open_timeout = 10
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
14
|
def tui
|
22
15
|
@tui ||= TUI.new($stdin, $stdout)
|
23
16
|
end
|
@@ -27,46 +20,65 @@ module Elelem
|
|
27
20
|
end
|
28
21
|
|
29
22
|
def logger
|
30
|
-
@logger ||= Logger.new(
|
31
|
-
|
23
|
+
@logger ||= Logger.new("#{Time.now.strftime("%Y-%m-%d")}-elelem.log").tap do |logger|
|
24
|
+
if debug
|
25
|
+
logger.level = :debug
|
26
|
+
else
|
27
|
+
logger.level = ENV.fetch("LOG_LEVEL", "warn")
|
28
|
+
end
|
29
|
+
logger.formatter = ->(severity, datetime, progname, message) {
|
30
|
+
timestamp = datetime.strftime("%H:%M:%S.%3N")
|
31
|
+
"[#{timestamp}] #{severity.ljust(5)} #{message.to_s.strip}\n"
|
32
|
+
}
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
|
-
def uri
|
36
|
-
@uri ||= URI("#{scheme}://#{host}/api/chat")
|
37
|
-
end
|
38
|
-
|
39
36
|
def conversation
|
40
|
-
@conversation ||= Conversation.new
|
37
|
+
@conversation ||= Conversation.new.tap do |conversation|
|
38
|
+
resources = mcp_clients.map do |client|
|
39
|
+
client.resources.map do |resource|
|
40
|
+
resource["uri"]
|
41
|
+
end
|
42
|
+
end.flatten
|
43
|
+
conversation.add(role: :tool, content: resources)
|
44
|
+
end
|
41
45
|
end
|
42
46
|
|
43
47
|
def tools
|
44
|
-
@tools ||= Tools.new(self,
|
48
|
+
@tools ||= Tools.new(self,
|
49
|
+
[
|
50
|
+
Toolbox::Exec.new(self),
|
51
|
+
Toolbox::File.new(self),
|
52
|
+
Toolbox::Web.new(self),
|
53
|
+
Toolbox::Prompt.new(self),
|
54
|
+
Toolbox::Memory.new(self),
|
55
|
+
] + mcp_tools
|
56
|
+
)
|
45
57
|
end
|
46
58
|
|
47
|
-
|
48
|
-
|
49
|
-
def scheme
|
50
|
-
host.match?(/\A(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?\z/) ? "http" : "https"
|
59
|
+
def cleanup
|
60
|
+
@mcp_clients&.each(&:shutdown)
|
51
61
|
end
|
52
62
|
|
53
|
-
|
54
|
-
return [] if ENV["SMALL"]
|
63
|
+
private
|
55
64
|
|
56
|
-
|
65
|
+
def mcp_tools
|
66
|
+
@mcp_tools ||= mcp_clients.map do |client|
|
67
|
+
client.tools.map do |tool|
|
68
|
+
Toolbox::MCP.new(client, tui, tool)
|
69
|
+
end
|
70
|
+
end.flatten
|
57
71
|
end
|
58
72
|
|
59
|
-
def
|
60
|
-
|
61
|
-
"
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
"--project", Dir.pwd
|
69
|
-
])
|
73
|
+
def mcp_clients
|
74
|
+
@mcp_clients ||= begin
|
75
|
+
config = Pathname.pwd.join(".mcp.json")
|
76
|
+
return [] unless config.exist?
|
77
|
+
|
78
|
+
JSON.parse(config.read).map do |_key, value|
|
79
|
+
MCPClient.new(self, [value["command"]] + value["args"])
|
80
|
+
end
|
81
|
+
end
|
70
82
|
end
|
71
83
|
end
|
72
84
|
end
|
data/lib/elelem/conversation.rb
CHANGED
@@ -21,7 +21,7 @@ module Elelem
|
|
21
21
|
if @items.last && @items.last[:role] == role
|
22
22
|
@items.last[:content] += content
|
23
23
|
else
|
24
|
-
@items.push({ role: role, content: content })
|
24
|
+
@items.push({ role: role, content: normalize(content) })
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
@@ -30,5 +30,13 @@ module Elelem
|
|
30
30
|
def system_prompt
|
31
31
|
ERB.new(Pathname.new(__dir__).join("system_prompt.erb").read).result(binding)
|
32
32
|
end
|
33
|
+
|
34
|
+
def normalize(content)
|
35
|
+
if content.is_a?(Array)
|
36
|
+
content.join(", ")
|
37
|
+
else
|
38
|
+
content.to_s
|
39
|
+
end
|
40
|
+
end
|
33
41
|
end
|
34
42
|
end
|
data/lib/elelem/mcp_client.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Elelem
|
4
4
|
class MCPClient
|
5
|
-
attr_reader :tools
|
5
|
+
attr_reader :tools, :resources
|
6
6
|
|
7
7
|
def initialize(configuration, command = [])
|
8
8
|
@configuration = configuration
|
@@ -12,7 +12,7 @@ module Elelem
|
|
12
12
|
send_request(
|
13
13
|
method: "initialize",
|
14
14
|
params: {
|
15
|
-
protocolVersion: "
|
15
|
+
protocolVersion: "2025-06-08",
|
16
16
|
capabilities: {
|
17
17
|
tools: {}
|
18
18
|
},
|
@@ -23,11 +23,12 @@ module Elelem
|
|
23
23
|
}
|
24
24
|
)
|
25
25
|
|
26
|
-
# 2. Send initialized notification (
|
26
|
+
# 2. Send initialized notification (optional for some MCP servers)
|
27
27
|
send_notification(method: "notifications/initialized")
|
28
28
|
|
29
29
|
# 3. Now we can request tools
|
30
30
|
@tools = send_request(method: "tools/list")&.dig("tools") || []
|
31
|
+
@resources = send_request(method: "resources/list")&.dig("resources") || []
|
31
32
|
end
|
32
33
|
|
33
34
|
def connected?
|
@@ -53,6 +54,33 @@ module Elelem
|
|
53
54
|
)
|
54
55
|
end
|
55
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
|
+
|
56
84
|
private
|
57
85
|
|
58
86
|
attr_reader :stdin, :stdout, :stderr, :worker, :configuration
|
@@ -86,6 +114,8 @@ module Elelem
|
|
86
114
|
end
|
87
115
|
|
88
116
|
def send_notification(method:, params: {})
|
117
|
+
return unless connected?
|
118
|
+
|
89
119
|
notification = {
|
90
120
|
jsonrpc: "2.0",
|
91
121
|
method: method
|
@@ -94,6 +124,13 @@ module Elelem
|
|
94
124
|
configuration.logger.debug("Sending notification: #{JSON.pretty_generate(notification)}")
|
95
125
|
@stdin.puts(JSON.generate(notification))
|
96
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
|
97
134
|
end
|
98
135
|
end
|
99
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
|
+
Thinking.new(agent, "*", :yellow)
|
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["reasoning"] && !message["reasoning"]&.empty?
|
9
|
+
agent.tui.say(message["reasoning"], 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
|