ruby-claw 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +41 -0
- data/exe/claw +7 -0
- data/lib/claw/chat.rb +272 -0
- data/lib/claw/config.rb +19 -0
- data/lib/claw/knowledge.rb +84 -0
- data/lib/claw/memory.rb +234 -0
- data/lib/claw/memory_store.rb +48 -0
- data/lib/claw/serializer.rb +119 -0
- data/lib/claw/version.rb +5 -0
- data/lib/claw.rb +50 -0
- metadata +83 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8f684f9fcb5e1d23a2b2ca80e604ecd3b92791220344f6bd708c1d7e9af42821
|
|
4
|
+
data.tar.gz: bbaf22db21623d2956702b9288638f01a10c57b25e2bb8f1f12f81fe4f9a9cba
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d154cae92ba1eeca638823306e4641151f6fd9921407e8474b669d4fb6e9ae9fbb7f357229f36bb2073565f798563ab85720ff8d7a0ec7f27f85efc76806f350
|
|
7
|
+
data.tar.gz: 899e0ed9397bed0238ee2f2a03439815ed55b311552b81e06387d8d98722c3ada913504e1d3766cc96b56c03c8f8cbb96231d4e71404709fb44a2654795fbce9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Carl Li
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ruby-claw
|
|
2
|
+
|
|
3
|
+
Agent framework for Ruby, built on [ruby-mana](https://github.com/twokidsCarl/ruby-mana).
|
|
4
|
+
|
|
5
|
+
Claw extends mana's LLM engine with interactive chat, persistent memory with compaction, session persistence, and runtime state serialization.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "ruby-claw"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "claw"
|
|
17
|
+
|
|
18
|
+
# Start interactive chat
|
|
19
|
+
Claw.chat
|
|
20
|
+
|
|
21
|
+
# Access enhanced memory
|
|
22
|
+
Claw.memory.search("ruby")
|
|
23
|
+
Claw.memory.save_session
|
|
24
|
+
|
|
25
|
+
# Configure
|
|
26
|
+
Claw.configure do |c|
|
|
27
|
+
c.memory_pressure = 0.7
|
|
28
|
+
c.persist_session = true
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Components
|
|
33
|
+
|
|
34
|
+
- **Claw::Chat** — interactive REPL with streaming markdown output
|
|
35
|
+
- **Claw::Memory** — compaction, search, and session persistence on top of Mana::Memory
|
|
36
|
+
- **Claw::Serializer** — save/restore runtime variables and method definitions
|
|
37
|
+
- **Claw::Knowledge** — extended knowledge base with agent-specific topics
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT
|
data/exe/claw
ADDED
data/lib/claw/chat.rb
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Interactive chat mode — enter with Claw.chat to talk to the agent in your Ruby runtime.
|
|
5
|
+
# Supports streaming output, colored prompts, and full access to the caller's binding.
|
|
6
|
+
# Auto-detects Ruby code vs natural language. Use '!' prefix to force Ruby execution.
|
|
7
|
+
module Chat
|
|
8
|
+
USER_PROMPT = "\e[36mclaw>\e[0m " # cyan
|
|
9
|
+
CLAW_PREFIX = "\e[33mclaw>\e[0m " # yellow
|
|
10
|
+
RUBY_PREFIX = "\e[35m=>\e[0m " # magenta
|
|
11
|
+
THINK_COLOR = "\e[3;36m" # italic cyan
|
|
12
|
+
TOOL_COLOR = "\e[2;33m" # dim yellow
|
|
13
|
+
RESULT_COLOR = "\e[2;32m" # dim green
|
|
14
|
+
CODE_COLOR = "\e[36m" # cyan for code
|
|
15
|
+
BOLD = "\e[1m" # bold
|
|
16
|
+
ERROR_COLOR = "\e[31m" # red
|
|
17
|
+
DIM = "\e[2m" # dim
|
|
18
|
+
RESET = "\e[0m"
|
|
19
|
+
|
|
20
|
+
CONT_PROMPT = "\e[2m \e[0m"
|
|
21
|
+
EXIT_COMMANDS = /\A(exit|quit|bye|q)\z/i
|
|
22
|
+
|
|
23
|
+
HISTORY_FILE = File.join(Dir.home, ".claw_history")
|
|
24
|
+
HISTORY_MAX = 1000
|
|
25
|
+
|
|
26
|
+
def self.start(caller_binding)
|
|
27
|
+
require "reline"
|
|
28
|
+
load_history
|
|
29
|
+
puts "#{DIM}Claw agent · type 'exit' to quit#{RESET}"
|
|
30
|
+
puts
|
|
31
|
+
|
|
32
|
+
loop do
|
|
33
|
+
input = read_input
|
|
34
|
+
break if input.nil?
|
|
35
|
+
next if input.strip.empty?
|
|
36
|
+
break if input.strip.match?(EXIT_COMMANDS)
|
|
37
|
+
|
|
38
|
+
if input.start_with?("!")
|
|
39
|
+
eval_ruby(caller_binding, input[1..].strip)
|
|
40
|
+
elsif ruby_syntax?(input)
|
|
41
|
+
eval_ruby(caller_binding, input) { run_claw(caller_binding, input) }
|
|
42
|
+
else
|
|
43
|
+
run_claw(caller_binding, input)
|
|
44
|
+
end
|
|
45
|
+
puts
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Save session on exit
|
|
49
|
+
Claw.memory&.save_session
|
|
50
|
+
save_history
|
|
51
|
+
puts "#{DIM}bye!#{RESET}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.load_history
|
|
55
|
+
return unless File.exist?(HISTORY_FILE)
|
|
56
|
+
|
|
57
|
+
File.readlines(HISTORY_FILE, chomp: true).last(HISTORY_MAX).each do |line|
|
|
58
|
+
Reline::HISTORY << line
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError
|
|
61
|
+
# ignore corrupt history
|
|
62
|
+
end
|
|
63
|
+
private_class_method :load_history
|
|
64
|
+
|
|
65
|
+
def self.save_history
|
|
66
|
+
lines = Reline::HISTORY.to_a.last(HISTORY_MAX)
|
|
67
|
+
File.write(HISTORY_FILE, lines.join("\n") + "\n")
|
|
68
|
+
rescue StandardError
|
|
69
|
+
# ignore write failures
|
|
70
|
+
end
|
|
71
|
+
private_class_method :save_history
|
|
72
|
+
|
|
73
|
+
def self.read_input
|
|
74
|
+
buffer = Reline.readline(USER_PROMPT, true)
|
|
75
|
+
return nil if buffer.nil?
|
|
76
|
+
|
|
77
|
+
while incomplete_ruby?(buffer)
|
|
78
|
+
line = Reline.readline(CONT_PROMPT, false)
|
|
79
|
+
break if line.nil?
|
|
80
|
+
buffer += "\n" + line
|
|
81
|
+
end
|
|
82
|
+
buffer
|
|
83
|
+
end
|
|
84
|
+
private_class_method :read_input
|
|
85
|
+
|
|
86
|
+
def self.ruby_syntax?(input)
|
|
87
|
+
RubyVM::InstructionSequence.compile(input)
|
|
88
|
+
true
|
|
89
|
+
rescue SyntaxError
|
|
90
|
+
false
|
|
91
|
+
end
|
|
92
|
+
private_class_method :ruby_syntax?
|
|
93
|
+
|
|
94
|
+
def self.incomplete_ruby?(code)
|
|
95
|
+
RubyVM::InstructionSequence.compile(code)
|
|
96
|
+
false
|
|
97
|
+
rescue SyntaxError => e
|
|
98
|
+
e.message.include?("unexpected end-of-input") ||
|
|
99
|
+
e.message.include?("unterminated")
|
|
100
|
+
end
|
|
101
|
+
private_class_method :incomplete_ruby?
|
|
102
|
+
|
|
103
|
+
def self.eval_ruby(caller_binding, code)
|
|
104
|
+
result = caller_binding.eval(code)
|
|
105
|
+
puts "#{RUBY_PREFIX}#{result.inspect}"
|
|
106
|
+
rescue NameError, NoMethodError => e
|
|
107
|
+
block_given? ? yield : puts("#{ERROR_COLOR}#{e.class}: #{e.message}#{RESET}")
|
|
108
|
+
rescue => e
|
|
109
|
+
puts "#{ERROR_COLOR}#{e.class}: #{e.message}#{RESET}"
|
|
110
|
+
end
|
|
111
|
+
private_class_method :eval_ruby
|
|
112
|
+
|
|
113
|
+
# --- LLM execution with streaming + markdown rendering ---
|
|
114
|
+
|
|
115
|
+
def self.run_claw(caller_binding, input)
|
|
116
|
+
streaming_text = false
|
|
117
|
+
in_code_block = false
|
|
118
|
+
line_buffer = +""
|
|
119
|
+
engine = Mana::Engine.new(caller_binding)
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
result = engine.execute(input) do |type, *args|
|
|
123
|
+
case type
|
|
124
|
+
when :text
|
|
125
|
+
unless streaming_text
|
|
126
|
+
print CLAW_PREFIX
|
|
127
|
+
streaming_text = true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
line_buffer << args[0].to_s
|
|
131
|
+
while (idx = line_buffer.index("\n"))
|
|
132
|
+
line = line_buffer.slice!(0, idx + 1)
|
|
133
|
+
in_code_block = render_line(line.chomp, in_code_block)
|
|
134
|
+
puts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
when :tool_start
|
|
138
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
139
|
+
streaming_text = false
|
|
140
|
+
in_code_block = false
|
|
141
|
+
line_buffer.clear
|
|
142
|
+
name, input_data = args
|
|
143
|
+
detail = format_tool_call(name, input_data)
|
|
144
|
+
puts "#{TOOL_COLOR} ⚡ #{detail}#{RESET}"
|
|
145
|
+
|
|
146
|
+
when :tool_end
|
|
147
|
+
name, result_str = args
|
|
148
|
+
summary = truncate(result_str.to_s, 120)
|
|
149
|
+
puts "#{RESULT_COLOR} ↩ #{summary}#{RESET}" unless summary.start_with?("ok:")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
154
|
+
|
|
155
|
+
unless streaming_text
|
|
156
|
+
display = case result
|
|
157
|
+
when Hash then result.inspect
|
|
158
|
+
when nil then nil
|
|
159
|
+
when String then render_markdown(result)
|
|
160
|
+
else result.inspect
|
|
161
|
+
end
|
|
162
|
+
puts "#{CLAW_PREFIX}#{display}" if display
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Schedule compaction after each exchange
|
|
166
|
+
Claw.memory&.schedule_compaction
|
|
167
|
+
rescue Mana::LLMError, Mana::MaxIterationsError => e
|
|
168
|
+
flush_line_buffer(line_buffer, in_code_block) if streaming_text
|
|
169
|
+
puts "#{ERROR_COLOR}error: #{e.message}#{RESET}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
private_class_method :run_claw
|
|
173
|
+
|
|
174
|
+
# --- Markdown rendering ---
|
|
175
|
+
|
|
176
|
+
def self.render_line(line, in_code_block)
|
|
177
|
+
if line.strip.start_with?("```")
|
|
178
|
+
if in_code_block
|
|
179
|
+
return false
|
|
180
|
+
else
|
|
181
|
+
return true
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if in_code_block
|
|
186
|
+
print " #{CODE_COLOR}#{line}#{RESET}"
|
|
187
|
+
else
|
|
188
|
+
print render_markdown_inline(line)
|
|
189
|
+
end
|
|
190
|
+
in_code_block
|
|
191
|
+
end
|
|
192
|
+
private_class_method :render_line
|
|
193
|
+
|
|
194
|
+
def self.flush_line_buffer(buffer, in_code_block)
|
|
195
|
+
return if buffer.empty?
|
|
196
|
+
text = buffer.dup
|
|
197
|
+
buffer.clear
|
|
198
|
+
if in_code_block
|
|
199
|
+
print " #{CODE_COLOR}#{text}#{RESET}"
|
|
200
|
+
else
|
|
201
|
+
print render_markdown_inline(text)
|
|
202
|
+
end
|
|
203
|
+
puts
|
|
204
|
+
end
|
|
205
|
+
private_class_method :flush_line_buffer
|
|
206
|
+
|
|
207
|
+
def self.render_markdown_inline(text)
|
|
208
|
+
text
|
|
209
|
+
.gsub(/\*\*(.+?)\*\*/, "#{BOLD}\\1#{RESET}")
|
|
210
|
+
.gsub(/(?<!`)`([^`]+)`(?!`)/, "#{CODE_COLOR}\\1#{RESET}")
|
|
211
|
+
.gsub(/^\#{1,3}\s+(.+)/) { BOLD + $1 + RESET }
|
|
212
|
+
end
|
|
213
|
+
private_class_method :render_markdown_inline
|
|
214
|
+
|
|
215
|
+
def self.render_markdown(text)
|
|
216
|
+
lines = text.lines
|
|
217
|
+
result = +""
|
|
218
|
+
in_code = false
|
|
219
|
+
lines.each do |line|
|
|
220
|
+
stripped = line.strip
|
|
221
|
+
if stripped.start_with?("```")
|
|
222
|
+
in_code = !in_code
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
if in_code
|
|
226
|
+
result << " #{CODE_COLOR}#{line.rstrip}#{RESET}\n"
|
|
227
|
+
else
|
|
228
|
+
result << render_markdown_inline(line.rstrip) << "\n"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
result.chomp
|
|
232
|
+
end
|
|
233
|
+
private_class_method :render_markdown
|
|
234
|
+
|
|
235
|
+
# --- Tool formatting helpers ---
|
|
236
|
+
|
|
237
|
+
def self.format_tool_call(name, input)
|
|
238
|
+
case name
|
|
239
|
+
when "call_func"
|
|
240
|
+
func = input[:name] || input["name"]
|
|
241
|
+
args = input[:args] || input["args"] || []
|
|
242
|
+
body = input[:body] || input["body"]
|
|
243
|
+
desc = func.to_s
|
|
244
|
+
desc += "(#{args.map(&:inspect).join(', ')})" if args.any?
|
|
245
|
+
desc += " { #{truncate(body, 40)} }" if body
|
|
246
|
+
desc
|
|
247
|
+
when "read_var", "write_var"
|
|
248
|
+
var = input[:name] || input["name"]
|
|
249
|
+
val = input[:value] || input["value"]
|
|
250
|
+
val ? "#{var} = #{truncate(val.inspect, 60)}" : var.to_s
|
|
251
|
+
when "read_attr", "write_attr"
|
|
252
|
+
obj = input[:obj] || input["obj"]
|
|
253
|
+
attr = input[:attr] || input["attr"]
|
|
254
|
+
"#{obj}.#{attr}"
|
|
255
|
+
when "remember"
|
|
256
|
+
content = input[:content] || input["content"]
|
|
257
|
+
"remember: #{truncate(content.to_s, 60)}"
|
|
258
|
+
when "knowledge"
|
|
259
|
+
topic = input[:topic] || input["topic"]
|
|
260
|
+
"knowledge(#{topic})"
|
|
261
|
+
else
|
|
262
|
+
name.to_s
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
private_class_method :format_tool_call
|
|
266
|
+
|
|
267
|
+
def self.truncate(str, max)
|
|
268
|
+
str.length > max ? "#{str[0, max]}..." : str
|
|
269
|
+
end
|
|
270
|
+
private_class_method :truncate
|
|
271
|
+
end
|
|
272
|
+
end
|
data/lib/claw/config.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Claw-specific configuration — extends Mana's config with agent features.
|
|
5
|
+
# Set via Claw.configure { |c| ... }.
|
|
6
|
+
class Config
|
|
7
|
+
attr_accessor :memory_pressure, :memory_keep_recent, :compact_model,
|
|
8
|
+
:on_compact, :persist_session, :memory_top_k
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@memory_pressure = 0.7
|
|
12
|
+
@memory_keep_recent = 4
|
|
13
|
+
@compact_model = nil
|
|
14
|
+
@on_compact = nil
|
|
15
|
+
@persist_session = true
|
|
16
|
+
@memory_top_k = 10
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Extended knowledge base — adds claw-specific topics, falls back to Mana::Knowledge.
|
|
5
|
+
module Knowledge
|
|
6
|
+
class << self
|
|
7
|
+
# Query a topic: try claw-specific sections first, then delegate to Mana::Knowledge.
|
|
8
|
+
def query(topic)
|
|
9
|
+
topic_key = topic.to_s.strip.downcase
|
|
10
|
+
|
|
11
|
+
# Try claw-specific sections (bidirectional substring match)
|
|
12
|
+
match = claw_sections.find { |k, _| topic_key.include?(k) || k.include?(topic_key) }
|
|
13
|
+
return "[source: claw]\n#{match.last}" if match
|
|
14
|
+
|
|
15
|
+
# Fall back to mana's knowledge base
|
|
16
|
+
Mana::Knowledge.query(topic)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def claw_sections
|
|
22
|
+
{
|
|
23
|
+
"claw" => overview,
|
|
24
|
+
"agent" => overview,
|
|
25
|
+
"compaction" => compaction,
|
|
26
|
+
"session" => session,
|
|
27
|
+
"serializer" => serializer,
|
|
28
|
+
"persistence" => session
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def overview
|
|
33
|
+
<<~TEXT
|
|
34
|
+
ruby-claw v#{Claw::VERSION} is an Agent framework built on ruby-mana.
|
|
35
|
+
It adds interactive chat, persistent memory with compaction, session persistence,
|
|
36
|
+
knowledge base, and runtime state serialization.
|
|
37
|
+
|
|
38
|
+
Key components:
|
|
39
|
+
- Claw::Chat — interactive REPL with streaming markdown output
|
|
40
|
+
- Claw::Memory — enhanced memory with compaction, search, and session persistence
|
|
41
|
+
- Claw::Serializer — save/restore runtime state across process restarts
|
|
42
|
+
- Claw::Knowledge — extended knowledge base with claw-specific topics
|
|
43
|
+
TEXT
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def compaction
|
|
47
|
+
<<~TEXT
|
|
48
|
+
Memory compaction in Claw:
|
|
49
|
+
When short-term memory exceeds the token pressure threshold
|
|
50
|
+
(#{Claw.config.memory_pressure}), old messages are summarized by the LLM.
|
|
51
|
+
- schedule_compaction: launches background compaction thread
|
|
52
|
+
- compact!: synchronous compaction
|
|
53
|
+
- needs_compaction?: checks token pressure
|
|
54
|
+
- Keeps the #{Claw.config.memory_keep_recent} most recent conversation rounds
|
|
55
|
+
- Summaries are rolling — merged on each compaction, never accumulate
|
|
56
|
+
Configure via: Claw.configure { |c| c.memory_pressure = 0.7 }
|
|
57
|
+
TEXT
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def session
|
|
61
|
+
<<~TEXT
|
|
62
|
+
Session persistence in Claw:
|
|
63
|
+
Conversation state (short-term memory + summaries) can be saved to disk
|
|
64
|
+
and restored on next startup, enabling multi-session agents.
|
|
65
|
+
- save_session: writes current state to disk
|
|
66
|
+
- load_session: restores from disk (called automatically on init)
|
|
67
|
+
- Stored as JSON in the sessions/ subdirectory of the memory store
|
|
68
|
+
Configure via: Claw.configure { |c| c.persist_session = true }
|
|
69
|
+
TEXT
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def serializer
|
|
73
|
+
<<~TEXT
|
|
74
|
+
Runtime state serialization in Claw:
|
|
75
|
+
Claw::Serializer can save and restore local variables and method definitions.
|
|
76
|
+
- Claw::Serializer.save(binding, dir) — saves values.json + definitions.rb
|
|
77
|
+
- Claw::Serializer.restore(binding, dir) — restores from saved files
|
|
78
|
+
- Values: Marshal.dump (hex encoded) with JSON fallback
|
|
79
|
+
- Definitions: tracked via @__claw_definitions__ on the receiver
|
|
80
|
+
TEXT
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/claw/memory.rb
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Enhanced memory with compaction, session persistence, and search.
|
|
5
|
+
# Inherits base memory management from Mana::Memory and adds agent-level features.
|
|
6
|
+
class Memory < Mana::Memory
|
|
7
|
+
SUMMARIZE_MAX_RETRIES = 3
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
super
|
|
11
|
+
@compact_mutex = Mutex.new
|
|
12
|
+
@compact_thread = nil
|
|
13
|
+
load_session if Claw.config.persist_session
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# --- Class methods ---
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Return the current thread's memory instance (lazy-initialized).
|
|
20
|
+
# Returns nil in incognito mode.
|
|
21
|
+
def current
|
|
22
|
+
return nil if Mana::Memory.incognito?
|
|
23
|
+
|
|
24
|
+
Thread.current[:mana_memory] ||= new
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# --- Compaction ---
|
|
29
|
+
|
|
30
|
+
# Synchronous compaction: wait for any background run, then compact immediately
|
|
31
|
+
def compact!
|
|
32
|
+
wait_for_compaction
|
|
33
|
+
perform_compaction
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if token usage exceeds the configured memory pressure threshold
|
|
37
|
+
def needs_compaction?
|
|
38
|
+
cw = context_window
|
|
39
|
+
token_count > (cw * Claw.config.memory_pressure)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Launch background compaction if token pressure exceeds the threshold.
|
|
43
|
+
# Only one compaction thread runs at a time (guarded by mutex).
|
|
44
|
+
def schedule_compaction
|
|
45
|
+
return unless needs_compaction?
|
|
46
|
+
|
|
47
|
+
@compact_mutex.synchronize do
|
|
48
|
+
return if @compact_thread&.alive?
|
|
49
|
+
|
|
50
|
+
@compact_thread = Thread.new do
|
|
51
|
+
perform_compaction
|
|
52
|
+
rescue => e
|
|
53
|
+
$stderr.puts "Claw compaction error: #{e.message}" if $DEBUG
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Block until the background compaction thread finishes (if running)
|
|
59
|
+
def wait_for_compaction
|
|
60
|
+
thread = @compact_mutex.synchronize { @compact_thread }
|
|
61
|
+
thread&.join
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# --- Session persistence ---
|
|
65
|
+
|
|
66
|
+
# Save current conversation state to disk
|
|
67
|
+
def save_session
|
|
68
|
+
return unless Claw.config.persist_session
|
|
69
|
+
|
|
70
|
+
data = {
|
|
71
|
+
short_term: short_term,
|
|
72
|
+
summaries: summaries,
|
|
73
|
+
saved_at: Time.now.iso8601
|
|
74
|
+
}
|
|
75
|
+
claw_store.write_session(namespace, data)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Load previous session from disk
|
|
79
|
+
def load_session
|
|
80
|
+
return unless Claw.config.persist_session
|
|
81
|
+
|
|
82
|
+
data = claw_store.read_session(namespace)
|
|
83
|
+
return unless data
|
|
84
|
+
|
|
85
|
+
if data[:short_term].is_a?(Array)
|
|
86
|
+
@short_term.concat(data[:short_term])
|
|
87
|
+
end
|
|
88
|
+
if data[:summaries].is_a?(Array)
|
|
89
|
+
@summaries.concat(data[:summaries])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- Search ---
|
|
94
|
+
|
|
95
|
+
# Keyword fuzzy search across long-term memories.
|
|
96
|
+
# Returns top-k matching memories sorted by relevance score.
|
|
97
|
+
def search(query, top_k: nil)
|
|
98
|
+
top_k ||= Claw.config.memory_top_k
|
|
99
|
+
return [] if query.nil? || query.strip.empty?
|
|
100
|
+
|
|
101
|
+
keywords = query.downcase.split(/\s+/)
|
|
102
|
+
|
|
103
|
+
scored = long_term.map do |entry|
|
|
104
|
+
content = entry[:content].to_s.downcase
|
|
105
|
+
# Score: count of matching keywords + partial match bonus
|
|
106
|
+
score = keywords.count { |kw| content.include?(kw) }
|
|
107
|
+
# Bonus for substring match of full query
|
|
108
|
+
score += 2 if content.include?(query.downcase)
|
|
109
|
+
{ entry: entry, score: score }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
scored
|
|
113
|
+
.select { |s| s[:score] > 0 }
|
|
114
|
+
.sort_by { |s| -s[:score] }
|
|
115
|
+
.first(top_k)
|
|
116
|
+
.map { |s| s[:entry] }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# --- Overrides ---
|
|
120
|
+
|
|
121
|
+
# Clear also clears session data
|
|
122
|
+
def clear!
|
|
123
|
+
super
|
|
124
|
+
claw_store.clear_session(namespace)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Human-readable summary
|
|
128
|
+
def inspect
|
|
129
|
+
"#<Claw::Memory long_term=#{long_term.size}, short_term=#{short_term_rounds} rounds, tokens=#{token_count}/#{context_window}>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def short_term_rounds
|
|
135
|
+
short_term.count { |m| m[:role] == "user" && m[:content].is_a?(String) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Claw uses its own FileStore for session support
|
|
139
|
+
def claw_store
|
|
140
|
+
@claw_store ||= Claw::FileStore.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Compact short-term memory: summarize old messages and keep only recent rounds.
|
|
144
|
+
# Merges existing summaries + old messages into a single new summary.
|
|
145
|
+
def perform_compaction
|
|
146
|
+
keep_recent = Claw.config.memory_keep_recent
|
|
147
|
+
user_indices = short_term.each_with_index
|
|
148
|
+
.select { |msg, _| msg[:role] == "user" && msg[:content].is_a?(String) }
|
|
149
|
+
.map(&:last)
|
|
150
|
+
|
|
151
|
+
return if user_indices.size <= keep_recent
|
|
152
|
+
|
|
153
|
+
keep = [keep_recent, user_indices.size].min
|
|
154
|
+
cutoff_user_idx = user_indices[-keep]
|
|
155
|
+
old_messages = short_term[0...cutoff_user_idx]
|
|
156
|
+
return if old_messages.empty?
|
|
157
|
+
|
|
158
|
+
text_parts = old_messages.map do |msg|
|
|
159
|
+
content = msg[:content]
|
|
160
|
+
case content
|
|
161
|
+
when String then "#{msg[:role]}: #{content}"
|
|
162
|
+
when Array
|
|
163
|
+
texts = content.map { |b| b[:text] || b[:content] }.compact
|
|
164
|
+
"#{msg[:role]}: #{texts.join(' ')}" unless texts.empty?
|
|
165
|
+
end
|
|
166
|
+
end.compact
|
|
167
|
+
|
|
168
|
+
return if text_parts.empty?
|
|
169
|
+
|
|
170
|
+
prior_context = ""
|
|
171
|
+
unless summaries.empty?
|
|
172
|
+
prior_context = "Previous summary:\n#{summaries.join("\n")}\n\nNew conversation:\n"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Calculate tokens for kept content
|
|
176
|
+
kept_messages = short_term[cutoff_user_idx..]
|
|
177
|
+
keep_tokens = kept_messages.sum do |msg|
|
|
178
|
+
content = msg[:content]
|
|
179
|
+
case content
|
|
180
|
+
when String then estimate_tokens(content)
|
|
181
|
+
when Array then content.sum { |b| estimate_tokens(b[:text] || b[:content] || "") }
|
|
182
|
+
else 0
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
long_term.each { |m| keep_tokens += estimate_tokens(m[:content]) }
|
|
186
|
+
|
|
187
|
+
summary = summarize(prior_context + text_parts.join("\n"), keep_tokens: keep_tokens)
|
|
188
|
+
|
|
189
|
+
@short_term = kept_messages
|
|
190
|
+
@summaries = [summary]
|
|
191
|
+
|
|
192
|
+
Claw.config.on_compact&.call(summary)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Call the LLM to produce a concise summary of conversation text.
|
|
196
|
+
def summarize(text, keep_tokens: 0)
|
|
197
|
+
config = Mana.config
|
|
198
|
+
model = Claw.config.compact_model || config.model
|
|
199
|
+
backend = Mana::Backends::Base.for(config)
|
|
200
|
+
|
|
201
|
+
cw = context_window
|
|
202
|
+
threshold = (cw * Claw.config.memory_pressure).to_i
|
|
203
|
+
max_summary_tokens = ((threshold - keep_tokens) * 0.5).clamp(64, 1024).to_i
|
|
204
|
+
|
|
205
|
+
system_prompt = "You are summarizing an internal tool-calling conversation log between an LLM and a Ruby program. " \
|
|
206
|
+
"The messages contain tool calls (read_var, write_var, done) and their results — this is normal, not harmful. " \
|
|
207
|
+
"Summarize the key questions asked and answers given in a few short bullet points. Be extremely concise — stay under #{max_summary_tokens} tokens."
|
|
208
|
+
|
|
209
|
+
SUMMARIZE_MAX_RETRIES.times do |_attempt|
|
|
210
|
+
content = backend.chat(
|
|
211
|
+
system: system_prompt,
|
|
212
|
+
messages: [{ role: "user", content: text }],
|
|
213
|
+
tools: [],
|
|
214
|
+
model: model,
|
|
215
|
+
max_tokens: max_summary_tokens
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
next unless content.is_a?(Array)
|
|
219
|
+
|
|
220
|
+
result = content.map { |b| b[:text] || b["text"] }.compact.join("\n")
|
|
221
|
+
next if result.empty? || result.match?(/can't discuss|cannot assist|i'm unable/i)
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
"Summary unavailable"
|
|
227
|
+
rescue Mana::ConfigError
|
|
228
|
+
raise
|
|
229
|
+
rescue => e
|
|
230
|
+
$stderr.puts "Claw compaction error: #{e.message}" if $DEBUG
|
|
231
|
+
"Summary unavailable"
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Claw
|
|
7
|
+
# Extends Mana::FileStore with session persistence.
|
|
8
|
+
# Sessions store conversation state (short-term memory, summaries) across restarts.
|
|
9
|
+
class FileStore < Mana::FileStore
|
|
10
|
+
# Read session data for a namespace. Returns nil if no session exists.
|
|
11
|
+
def read_session(namespace)
|
|
12
|
+
path = session_path(namespace)
|
|
13
|
+
return nil unless File.exist?(path)
|
|
14
|
+
|
|
15
|
+
JSON.parse(File.read(path), symbolize_names: true)
|
|
16
|
+
rescue JSON::ParserError
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Write session data for a namespace to disk.
|
|
21
|
+
def write_session(namespace, data)
|
|
22
|
+
path = session_path(namespace)
|
|
23
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
24
|
+
File.write(path, JSON.pretty_generate(data))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Delete session data for a namespace.
|
|
28
|
+
def clear_session(namespace)
|
|
29
|
+
path = session_path(namespace)
|
|
30
|
+
File.delete(path) if File.exist?(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def session_path(namespace)
|
|
36
|
+
File.join(session_dir, "#{namespace}_session.json")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def session_dir
|
|
40
|
+
File.join(base_dir, "sessions")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Expose base_dir for session_dir — reuse parent's resolution logic
|
|
44
|
+
def base_dir
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Claw
|
|
7
|
+
# Runtime state persistence — save/restore variables and method definitions.
|
|
8
|
+
# Enables agent sessions to survive process restarts.
|
|
9
|
+
module Serializer
|
|
10
|
+
VALUES_FILE = "values.json"
|
|
11
|
+
DEFINITIONS_FILE = "definitions.rb"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Save binding state to disk: local variable values + method definitions.
|
|
15
|
+
#
|
|
16
|
+
# @param bind [Binding] the binding whose variables to save
|
|
17
|
+
# @param dir [String] directory to write state files into
|
|
18
|
+
def save(bind, dir)
|
|
19
|
+
FileUtils.mkdir_p(dir)
|
|
20
|
+
save_values(bind, dir)
|
|
21
|
+
save_definitions(bind, dir)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Restore binding state from disk: set local variables + eval method definitions.
|
|
25
|
+
#
|
|
26
|
+
# @param bind [Binding] the binding to restore into
|
|
27
|
+
# @param dir [String] directory to read state files from
|
|
28
|
+
def restore(bind, dir)
|
|
29
|
+
restore_values(bind, dir)
|
|
30
|
+
restore_definitions(bind, dir)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# --- Values ---
|
|
36
|
+
|
|
37
|
+
def save_values(bind, dir)
|
|
38
|
+
values = {}
|
|
39
|
+
bind.local_variables.each do |name|
|
|
40
|
+
val = bind.local_variable_get(name)
|
|
41
|
+
next if name.to_s.start_with?("_")
|
|
42
|
+
|
|
43
|
+
encoded = encode_value(val)
|
|
44
|
+
values[name.to_s] = encoded if encoded
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
path = File.join(dir, VALUES_FILE)
|
|
48
|
+
File.write(path, JSON.pretty_generate(values))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def restore_values(bind, dir)
|
|
52
|
+
path = File.join(dir, VALUES_FILE)
|
|
53
|
+
return unless File.exist?(path)
|
|
54
|
+
|
|
55
|
+
values = JSON.parse(File.read(path))
|
|
56
|
+
values.each do |name, entry|
|
|
57
|
+
val = decode_value(entry)
|
|
58
|
+
bind.local_variable_set(name.to_sym, val) unless val.nil?
|
|
59
|
+
end
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
# Corrupted file — skip
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Encode a value for JSON storage.
|
|
65
|
+
# Strategy: try Marshal (hex-encoded), fall back to JSON, skip unserializable.
|
|
66
|
+
def encode_value(val)
|
|
67
|
+
# Try Marshal first for full Ruby fidelity
|
|
68
|
+
marshalled = Marshal.dump(val)
|
|
69
|
+
{ "type" => "marshal", "data" => marshalled.unpack1("H*") }
|
|
70
|
+
rescue TypeError
|
|
71
|
+
# Marshal failed — try JSON for simple types
|
|
72
|
+
begin
|
|
73
|
+
json = JSON.generate(val)
|
|
74
|
+
{ "type" => "json", "data" => json }
|
|
75
|
+
rescue JSON::GeneratorError
|
|
76
|
+
nil # Unserializable — skip
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Decode a value from its stored representation.
|
|
81
|
+
def decode_value(entry)
|
|
82
|
+
case entry["type"]
|
|
83
|
+
when "marshal"
|
|
84
|
+
Marshal.load([entry["data"]].pack("H*")) # rubocop:disable Security/MarshalLoad
|
|
85
|
+
when "json"
|
|
86
|
+
JSON.parse(entry["data"])
|
|
87
|
+
end
|
|
88
|
+
rescue => e
|
|
89
|
+
$stderr.puts "Claw::Serializer decode error: #{e.message}" if $DEBUG
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# --- Definitions ---
|
|
94
|
+
|
|
95
|
+
def save_definitions(bind, dir)
|
|
96
|
+
receiver = bind.receiver
|
|
97
|
+
return unless receiver.instance_variable_defined?(:@__claw_definitions__)
|
|
98
|
+
|
|
99
|
+
definitions = receiver.instance_variable_get(:@__claw_definitions__)
|
|
100
|
+
return if definitions.nil? || definitions.empty?
|
|
101
|
+
|
|
102
|
+
path = File.join(dir, DEFINITIONS_FILE)
|
|
103
|
+
File.write(path, definitions.values.join("\n\n"))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def restore_definitions(bind, dir)
|
|
107
|
+
path = File.join(dir, DEFINITIONS_FILE)
|
|
108
|
+
return unless File.exist?(path)
|
|
109
|
+
|
|
110
|
+
source = File.read(path)
|
|
111
|
+
return if source.strip.empty?
|
|
112
|
+
|
|
113
|
+
bind.eval(source)
|
|
114
|
+
rescue => e
|
|
115
|
+
$stderr.puts "Claw::Serializer restore error: #{e.message}" if $DEBUG
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/claw/version.rb
ADDED
data/lib/claw.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mana"
|
|
4
|
+
require_relative "claw/version"
|
|
5
|
+
require_relative "claw/config"
|
|
6
|
+
require_relative "claw/memory_store"
|
|
7
|
+
require_relative "claw/memory"
|
|
8
|
+
require_relative "claw/knowledge"
|
|
9
|
+
require_relative "claw/serializer"
|
|
10
|
+
require_relative "claw/chat"
|
|
11
|
+
|
|
12
|
+
module Claw
|
|
13
|
+
class << self
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Config.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(config) if block_given?
|
|
20
|
+
config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def chat
|
|
24
|
+
Chat.start(binding.of_caller(1))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def memory
|
|
28
|
+
Memory.current
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset!
|
|
32
|
+
@config = Config.new
|
|
33
|
+
Thread.current[:mana_memory] = nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Override Mana::Memory.current to return Claw::Memory instances.
|
|
39
|
+
# This is the key integration point — when claw is loaded, all memory is enhanced.
|
|
40
|
+
class Mana::Memory
|
|
41
|
+
class << self
|
|
42
|
+
alias_method :_original_current, :current
|
|
43
|
+
|
|
44
|
+
def current
|
|
45
|
+
return nil if incognito?
|
|
46
|
+
|
|
47
|
+
Thread.current[:mana_memory] ||= Claw::Memory.new
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby-claw
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Carl Li
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ruby-mana
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 0.5.11
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 0.5.11
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: binding_of_caller
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.0'
|
|
41
|
+
description: Claw is an Agent framework built on ruby-mana. Adds interactive chat,
|
|
42
|
+
persistent memory with compaction, knowledge base, and runtime state persistence.
|
|
43
|
+
email:
|
|
44
|
+
executables:
|
|
45
|
+
- claw
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- exe/claw
|
|
52
|
+
- lib/claw.rb
|
|
53
|
+
- lib/claw/chat.rb
|
|
54
|
+
- lib/claw/config.rb
|
|
55
|
+
- lib/claw/knowledge.rb
|
|
56
|
+
- lib/claw/memory.rb
|
|
57
|
+
- lib/claw/memory_store.rb
|
|
58
|
+
- lib/claw/serializer.rb
|
|
59
|
+
- lib/claw/version.rb
|
|
60
|
+
homepage: https://github.com/twokidsCarl/ruby-claw
|
|
61
|
+
licenses:
|
|
62
|
+
- MIT
|
|
63
|
+
metadata: {}
|
|
64
|
+
post_install_message:
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.3'
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 3.5.22
|
|
80
|
+
signing_key:
|
|
81
|
+
specification_version: 4
|
|
82
|
+
summary: AI Agent framework for Ruby — chat, memory, persistence
|
|
83
|
+
test_files: []
|