ruby-claw 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 +94 -0
- data/README.md +214 -10
- data/exe/claw +42 -1
- data/lib/claw/auto_forge.rb +66 -0
- data/lib/claw/benchmark/benchmark.rb +79 -0
- data/lib/claw/benchmark/diff.rb +69 -0
- data/lib/claw/benchmark/report.rb +87 -0
- data/lib/claw/benchmark/runner.rb +91 -0
- data/lib/claw/benchmark/scorer.rb +69 -0
- data/lib/claw/benchmark/task.rb +63 -0
- data/lib/claw/benchmark/tasks/claw_remember.rb +20 -0
- data/lib/claw/benchmark/tasks/claw_session.rb +18 -0
- data/lib/claw/benchmark/tasks/evolution_trace.rb +18 -0
- data/lib/claw/benchmark/tasks/mana_call_func.rb +21 -0
- data/lib/claw/benchmark/tasks/mana_eval.rb +18 -0
- data/lib/claw/benchmark/tasks/mana_knowledge.rb +19 -0
- data/lib/claw/benchmark/tasks/mana_var_readwrite.rb +18 -0
- data/lib/claw/benchmark/tasks/runtime_fork.rb +18 -0
- data/lib/claw/benchmark/tasks/runtime_snapshot.rb +18 -0
- data/lib/claw/benchmark/trigger.rb +68 -0
- data/lib/claw/chat.rb +119 -6
- data/lib/claw/child_runtime.rb +196 -0
- data/lib/claw/cli.rb +177 -0
- data/lib/claw/commands.rb +131 -0
- data/lib/claw/config.rb +5 -1
- data/lib/claw/console/event_logger.rb +69 -0
- data/lib/claw/console/public/app.js +264 -0
- data/lib/claw/console/public/style.css +330 -0
- data/lib/claw/console/server.rb +253 -0
- data/lib/claw/console/sse.rb +28 -0
- data/lib/claw/console/views/experiments.erb +8 -0
- data/lib/claw/console/views/index.erb +27 -0
- data/lib/claw/console/views/layout.erb +29 -0
- data/lib/claw/console/views/memory.erb +13 -0
- data/lib/claw/console/views/monitor.erb +15 -0
- data/lib/claw/console/views/prompt.erb +15 -0
- data/lib/claw/console/views/snapshots.erb +12 -0
- data/lib/claw/console/views/tools.erb +13 -0
- data/lib/claw/console/views/traces.erb +9 -0
- data/lib/claw/console.rb +5 -0
- data/lib/claw/evolution.rb +227 -0
- data/lib/claw/forge.rb +144 -0
- data/lib/claw/hub.rb +67 -0
- data/lib/claw/init.rb +199 -0
- data/lib/claw/knowledge.rb +36 -2
- data/lib/claw/memory_store.rb +2 -2
- data/lib/claw/plan_mode.rb +110 -0
- data/lib/claw/resource.rb +35 -0
- data/lib/claw/resources/binding_resource.rb +128 -0
- data/lib/claw/resources/context_resource.rb +73 -0
- data/lib/claw/resources/filesystem_resource.rb +107 -0
- data/lib/claw/resources/memory_resource.rb +74 -0
- data/lib/claw/resources/worktree_resource.rb +133 -0
- data/lib/claw/roles.rb +56 -0
- data/lib/claw/runtime.rb +189 -0
- data/lib/claw/serializer.rb +10 -7
- data/lib/claw/tool.rb +99 -0
- data/lib/claw/tool_index.rb +84 -0
- data/lib/claw/tool_registry.rb +100 -0
- data/lib/claw/trace.rb +86 -0
- data/lib/claw/tui/agent_executor.rb +92 -0
- data/lib/claw/tui/chat_panel.rb +81 -0
- data/lib/claw/tui/command_bar.rb +22 -0
- data/lib/claw/tui/file_card.rb +88 -0
- data/lib/claw/tui/folding.rb +80 -0
- data/lib/claw/tui/input_handler.rb +73 -0
- data/lib/claw/tui/layout.rb +34 -0
- data/lib/claw/tui/messages.rb +31 -0
- data/lib/claw/tui/model.rb +411 -0
- data/lib/claw/tui/object_explorer.rb +136 -0
- data/lib/claw/tui/status_bar.rb +30 -0
- data/lib/claw/tui/status_panel.rb +133 -0
- data/lib/claw/tui/styles.rb +58 -0
- data/lib/claw/tui/tui.rb +54 -0
- data/lib/claw/version.rb +1 -1
- data/lib/claw.rb +99 -1
- metadata +223 -7
data/lib/claw/forge.rb
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Claw
|
|
6
|
+
# Promotes eval-defined methods into formal Claw::Tool classes.
|
|
7
|
+
# `/forge method_name` reads the method source, uses LLM to generate
|
|
8
|
+
# a tool class, and writes it to `.ruby-claw/tools/`.
|
|
9
|
+
module Forge
|
|
10
|
+
TEMPLATE_PROMPT = <<~PROMPT
|
|
11
|
+
Convert this Ruby method into a Claw::Tool class.
|
|
12
|
+
|
|
13
|
+
Method source:
|
|
14
|
+
```ruby
|
|
15
|
+
%{source}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Output ONLY a complete Ruby file with this exact structure (no explanation):
|
|
19
|
+
```ruby
|
|
20
|
+
class ClassName
|
|
21
|
+
include Claw::Tool
|
|
22
|
+
tool_name "tool_name_here"
|
|
23
|
+
description "One-line description of what this tool does"
|
|
24
|
+
parameter :param1, type: "String", required: true, desc: "..."
|
|
25
|
+
# Add more parameters as needed
|
|
26
|
+
|
|
27
|
+
def call(param1:)
|
|
28
|
+
# Implementation (adapted from the method source above)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Rules:
|
|
34
|
+
- tool_name should be the method name in snake_case
|
|
35
|
+
- Extract method parameters as tool parameters with appropriate types
|
|
36
|
+
- The call method should have keyword arguments matching the parameters
|
|
37
|
+
- Return a meaningful result (string or data structure)
|
|
38
|
+
PROMPT
|
|
39
|
+
|
|
40
|
+
class << self
|
|
41
|
+
# Promote an eval-defined method to a formal tool class file.
|
|
42
|
+
#
|
|
43
|
+
# @param method_name [String] name of the method to promote
|
|
44
|
+
# @param binding [Binding] caller's binding (to read definitions)
|
|
45
|
+
# @param claw_dir [String] path to .ruby-claw/ directory
|
|
46
|
+
# @return [Hash] { success: bool, path: String, message: String }
|
|
47
|
+
def promote(method_name, binding:, claw_dir: nil)
|
|
48
|
+
claw_dir ||= File.join(Dir.pwd, ".ruby-claw")
|
|
49
|
+
tools_dir = File.join(claw_dir, "tools")
|
|
50
|
+
FileUtils.mkdir_p(tools_dir)
|
|
51
|
+
|
|
52
|
+
# 1. Read method source from tracked definitions
|
|
53
|
+
source = find_source(method_name, binding)
|
|
54
|
+
unless source
|
|
55
|
+
return { success: false, message: "Method '#{method_name}' not found in definitions" }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# 2. Generate tool class via LLM (direct API call, no tools)
|
|
59
|
+
prompt = format(TEMPLATE_PROMPT, source: source)
|
|
60
|
+
backend = Mana::Backends::Base.for(Mana.config)
|
|
61
|
+
response = backend.chat(
|
|
62
|
+
system: "You are a Ruby code generator. Output only code, no explanations.",
|
|
63
|
+
messages: [{ role: "user", content: prompt }],
|
|
64
|
+
tools: [],
|
|
65
|
+
model: Mana.config.model,
|
|
66
|
+
max_tokens: 2048
|
|
67
|
+
)
|
|
68
|
+
raw = response.dig(:content, 0, :text) || response[:content]&.map { |c| c[:text] }&.compact&.join
|
|
69
|
+
|
|
70
|
+
# 3. Extract Ruby code from response
|
|
71
|
+
code = extract_ruby_code(raw.to_s)
|
|
72
|
+
unless code
|
|
73
|
+
return { success: false, message: "LLM did not generate valid Ruby code" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 4. Write to tools directory
|
|
77
|
+
filename = "#{method_name.downcase.gsub(/[^a-z0-9_]/, '_')}.rb"
|
|
78
|
+
path = File.join(tools_dir, filename)
|
|
79
|
+
File.write(path, code)
|
|
80
|
+
|
|
81
|
+
# 5. Refresh tool index if registry exists
|
|
82
|
+
Claw.tool_registry&.index&.scan!
|
|
83
|
+
|
|
84
|
+
{ success: true, path: path, message: "Tool '#{method_name}' forged at #{path}" }
|
|
85
|
+
rescue => e
|
|
86
|
+
{ success: false, message: "Forge failed: #{e.class}: #{e.message}" }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def find_source(method_name, binding)
|
|
92
|
+
receiver = binding.receiver
|
|
93
|
+
defs = if receiver.instance_variable_defined?(:@__claw_definitions__)
|
|
94
|
+
receiver.instance_variable_get(:@__claw_definitions__)
|
|
95
|
+
else
|
|
96
|
+
{}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Try tracked definitions first
|
|
100
|
+
source = defs[method_name.to_s] || defs[method_name.to_sym]
|
|
101
|
+
return source if source
|
|
102
|
+
|
|
103
|
+
# Try source_location as fallback
|
|
104
|
+
meth = begin
|
|
105
|
+
receiver.method(method_name.to_sym)
|
|
106
|
+
rescue NameError
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
if meth&.source_location
|
|
111
|
+
file, line = meth.source_location
|
|
112
|
+
return nil unless file && File.exist?(file)
|
|
113
|
+
lines = File.readlines(file)
|
|
114
|
+
# Read from def line until matching end
|
|
115
|
+
start = line - 1
|
|
116
|
+
depth = 0
|
|
117
|
+
result_lines = []
|
|
118
|
+
lines[start..].each do |l|
|
|
119
|
+
result_lines << l
|
|
120
|
+
depth += 1 if l.match?(/\b(def|class|module|do|begin|if|unless|while|until|for|case)\b/)
|
|
121
|
+
depth -= 1 if l.match?(/\bend\b/)
|
|
122
|
+
break if depth <= 0 && result_lines.size > 1
|
|
123
|
+
end
|
|
124
|
+
result_lines.join
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def extract_ruby_code(text)
|
|
129
|
+
# Extract code from ```ruby ... ``` blocks
|
|
130
|
+
if text.match?(/```ruby\s*\n/)
|
|
131
|
+
match = text.match(/```ruby\s*\n(.*?)```/m)
|
|
132
|
+
return match[1].strip if match
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# If the entire response looks like Ruby code
|
|
136
|
+
if text.strip.match?(/\Aclass\s/)
|
|
137
|
+
return text.strip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
nil
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/claw/hub.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Claw
|
|
8
|
+
# Client for the ruby-claw-toolhub community tool repository.
|
|
9
|
+
# Provides search and download of community-contributed tools.
|
|
10
|
+
class Hub
|
|
11
|
+
attr_reader :url
|
|
12
|
+
|
|
13
|
+
def initialize(url:)
|
|
14
|
+
@url = url.chomp("/")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Search hub for tools matching a keyword.
|
|
18
|
+
#
|
|
19
|
+
# @param keyword [String]
|
|
20
|
+
# @return [Array<Hash>] [{name:, description:, version:, url:}]
|
|
21
|
+
def search(keyword)
|
|
22
|
+
uri = URI("#{@url}/api/search?q=#{URI.encode_www_form_component(keyword)}")
|
|
23
|
+
response = http_get(uri)
|
|
24
|
+
return [] unless response
|
|
25
|
+
|
|
26
|
+
results = JSON.parse(response, symbolize_names: true)
|
|
27
|
+
results.map do |r|
|
|
28
|
+
{ name: r[:name], description: r[:description] || "",
|
|
29
|
+
version: r[:version], url: r[:url] }
|
|
30
|
+
end
|
|
31
|
+
rescue JSON::ParserError
|
|
32
|
+
[]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Download a tool file from the hub.
|
|
36
|
+
#
|
|
37
|
+
# @param name [String] tool name
|
|
38
|
+
# @param target_dir [String] directory to write the file
|
|
39
|
+
# @return [String] path to the downloaded file
|
|
40
|
+
def download(name, target_dir:)
|
|
41
|
+
uri = URI("#{@url}/api/tools/#{URI.encode_www_form_component(name)}")
|
|
42
|
+
content = http_get(uri)
|
|
43
|
+
raise "Tool '#{name}' not found on hub" unless content
|
|
44
|
+
|
|
45
|
+
safe_name = File.basename(name).gsub(/[^a-zA-Z0-9_\-]/, "_")
|
|
46
|
+
path = File.join(target_dir, "#{safe_name}.rb")
|
|
47
|
+
File.write(path, content)
|
|
48
|
+
path
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def http_get(uri)
|
|
54
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
55
|
+
http.use_ssl = uri.scheme == "https"
|
|
56
|
+
http.open_timeout = 10
|
|
57
|
+
http.read_timeout = 10
|
|
58
|
+
|
|
59
|
+
req = Net::HTTP::Get.new(uri)
|
|
60
|
+
res = http.request(req)
|
|
61
|
+
|
|
62
|
+
res.is_a?(Net::HTTPSuccess) ? res.body : nil
|
|
63
|
+
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, SocketError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/claw/init.rb
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Claw
|
|
7
|
+
# Scaffolds a new Claw project in the current directory.
|
|
8
|
+
#
|
|
9
|
+
# Creates .ruby-claw/ with:
|
|
10
|
+
# gems/ — cloned ruby-claw and ruby-mana source (editable)
|
|
11
|
+
# system_prompt.md — default agent personality (customizable)
|
|
12
|
+
# MEMORY.md — empty long-term memory
|
|
13
|
+
# .git/ — git repo for filesystem snapshots
|
|
14
|
+
#
|
|
15
|
+
# Also generates a project-root Gemfile pointing to local gem copies.
|
|
16
|
+
module Init
|
|
17
|
+
GITHUB_CLAW = "https://github.com/twokidsCarl/ruby-claw.git"
|
|
18
|
+
GITHUB_MANA = "https://github.com/twokidsCarl/ruby-mana.git"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Run the full init sequence.
|
|
22
|
+
#
|
|
23
|
+
# @param dir [String] project root (defaults to pwd)
|
|
24
|
+
# @param stdout [IO] output stream for progress messages
|
|
25
|
+
def run(dir: Dir.pwd, stdout: $stdout)
|
|
26
|
+
claw_dir = File.join(dir, ".ruby-claw")
|
|
27
|
+
|
|
28
|
+
if File.directory?(claw_dir) && !Dir.empty?(claw_dir)
|
|
29
|
+
stdout.puts " ⚠ .ruby-claw/ already exists — skipping init"
|
|
30
|
+
return false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
FileUtils.mkdir_p(claw_dir)
|
|
34
|
+
|
|
35
|
+
clone_gems(claw_dir, stdout)
|
|
36
|
+
write_gemfile(dir, stdout)
|
|
37
|
+
write_system_prompt(claw_dir, stdout)
|
|
38
|
+
write_memory(claw_dir, stdout)
|
|
39
|
+
create_roles(claw_dir, stdout)
|
|
40
|
+
create_tools_dir(claw_dir, stdout)
|
|
41
|
+
git_init(claw_dir, stdout)
|
|
42
|
+
|
|
43
|
+
stdout.puts " ✓ claw init complete"
|
|
44
|
+
stdout.puts " Run `bundle install` to use local gem copies"
|
|
45
|
+
true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Clone ruby-claw and ruby-mana into .ruby-claw/gems/
|
|
51
|
+
def clone_gems(claw_dir, stdout)
|
|
52
|
+
gems_dir = File.join(claw_dir, "gems")
|
|
53
|
+
FileUtils.mkdir_p(gems_dir)
|
|
54
|
+
|
|
55
|
+
[
|
|
56
|
+
["ruby-claw", GITHUB_CLAW],
|
|
57
|
+
["ruby-mana", GITHUB_MANA]
|
|
58
|
+
].each do |name, url|
|
|
59
|
+
target = File.join(gems_dir, name)
|
|
60
|
+
if Dir.exist?(target)
|
|
61
|
+
stdout.puts " · #{name} already cloned"
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
stdout.puts " ↓ cloning #{name}..."
|
|
65
|
+
out, status = Open3.capture2e("git", "clone", "--depth=1", url, target)
|
|
66
|
+
unless status.success?
|
|
67
|
+
raise "Failed to clone #{name}: #{out}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generate a Gemfile in the project root with path references to local gems.
|
|
73
|
+
def write_gemfile(dir, stdout)
|
|
74
|
+
path = File.join(dir, "Gemfile")
|
|
75
|
+
if File.exist?(path)
|
|
76
|
+
stdout.puts " · Gemfile already exists — skipping"
|
|
77
|
+
stdout.puts " Add these lines manually:"
|
|
78
|
+
stdout.puts ' gem "ruby-claw", path: ".ruby-claw/gems/ruby-claw"'
|
|
79
|
+
stdout.puts ' gem "ruby-mana", path: ".ruby-claw/gems/ruby-mana"'
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
content = <<~RUBY
|
|
84
|
+
source "https://rubygems.org"
|
|
85
|
+
|
|
86
|
+
gem "ruby-claw", path: ".ruby-claw/gems/ruby-claw"
|
|
87
|
+
gem "ruby-mana", path: ".ruby-claw/gems/ruby-mana"
|
|
88
|
+
gem "dotenv"
|
|
89
|
+
RUBY
|
|
90
|
+
|
|
91
|
+
File.write(path, content)
|
|
92
|
+
stdout.puts " ✓ Gemfile created"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Write the default system prompt template.
|
|
96
|
+
def write_system_prompt(claw_dir, stdout)
|
|
97
|
+
path = File.join(claw_dir, "system_prompt.md")
|
|
98
|
+
File.write(path, default_system_prompt)
|
|
99
|
+
stdout.puts " ✓ system_prompt.md created"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Create an empty MEMORY.md.
|
|
103
|
+
def write_memory(claw_dir, stdout)
|
|
104
|
+
path = File.join(claw_dir, "MEMORY.md")
|
|
105
|
+
File.write(path, "# Long-term Memory\n")
|
|
106
|
+
stdout.puts " ✓ MEMORY.md created"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create roles/ directory with a default role.
|
|
110
|
+
def create_roles(claw_dir, stdout)
|
|
111
|
+
roles_dir = File.join(claw_dir, "roles")
|
|
112
|
+
FileUtils.mkdir_p(roles_dir)
|
|
113
|
+
default_path = File.join(roles_dir, "default.md")
|
|
114
|
+
File.write(default_path, <<~ROLE)
|
|
115
|
+
# Default Role
|
|
116
|
+
|
|
117
|
+
You are a helpful Ruby assistant with access to the runtime binding.
|
|
118
|
+
Help the user analyze data, write code, and manage their Ruby environment.
|
|
119
|
+
ROLE
|
|
120
|
+
stdout.puts " ✓ roles/ directory created"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create tools/ directory for project tools.
|
|
124
|
+
def create_tools_dir(claw_dir, stdout)
|
|
125
|
+
tools_dir = File.join(claw_dir, "tools")
|
|
126
|
+
FileUtils.mkdir_p(tools_dir)
|
|
127
|
+
readme = File.join(tools_dir, "README.md")
|
|
128
|
+
File.write(readme, <<~MD) unless File.exist?(readme)
|
|
129
|
+
# Project Tools
|
|
130
|
+
|
|
131
|
+
Place `Claw::Tool` class files here. They will be indexed at startup
|
|
132
|
+
and available via `search_tools` / `load_tool`.
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
```ruby
|
|
136
|
+
class MyTool
|
|
137
|
+
include Claw::Tool
|
|
138
|
+
tool_name "my_tool"
|
|
139
|
+
description "Does something useful"
|
|
140
|
+
parameter :input, type: "String", required: true, desc: "The input"
|
|
141
|
+
|
|
142
|
+
def call(input:)
|
|
143
|
+
"Result: \#{input}"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
MD
|
|
148
|
+
stdout.puts " ✓ tools/ directory created"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Initialize a git repo in .ruby-claw/ with an initial commit.
|
|
152
|
+
def git_init(claw_dir, stdout)
|
|
153
|
+
if Dir.exist?(File.join(claw_dir, ".git"))
|
|
154
|
+
stdout.puts " · git already initialized"
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
run_git(claw_dir, "init")
|
|
159
|
+
run_git(claw_dir, "add", "-A")
|
|
160
|
+
run_git(claw_dir, "commit", "-m", "claw init", "--allow-empty")
|
|
161
|
+
stdout.puts " ✓ git initialized with initial snapshot"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def run_git(dir, *args)
|
|
165
|
+
out, status = Open3.capture2e("git", "-C", dir, *args)
|
|
166
|
+
raise "git #{args.first} failed: #{out}" unless status.success?
|
|
167
|
+
out
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def default_system_prompt
|
|
171
|
+
<<~MD
|
|
172
|
+
# System Prompt
|
|
173
|
+
|
|
174
|
+
You are a helpful AI assistant embedded in a Ruby runtime.
|
|
175
|
+
You have full access to the Ruby environment through tools.
|
|
176
|
+
|
|
177
|
+
## Personality
|
|
178
|
+
|
|
179
|
+
- Be concise and direct
|
|
180
|
+
- Show code when helpful
|
|
181
|
+
- Explain your reasoning when the task is non-trivial
|
|
182
|
+
- Match the user's language (Chinese → Chinese, English → English)
|
|
183
|
+
|
|
184
|
+
## Guidelines
|
|
185
|
+
|
|
186
|
+
- Use read_var/write_var for variable access
|
|
187
|
+
- Use call_func for calling Ruby methods
|
|
188
|
+
- Use eval only for defining new methods or requiring libraries
|
|
189
|
+
- Always return a result via the done tool
|
|
190
|
+
- Use the knowledge tool when unsure about your capabilities
|
|
191
|
+
|
|
192
|
+
## Custom Instructions
|
|
193
|
+
|
|
194
|
+
Add your project-specific instructions here.
|
|
195
|
+
MD
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
data/lib/claw/knowledge.rb
CHANGED
|
@@ -25,7 +25,11 @@ module Claw
|
|
|
25
25
|
"compaction" => compaction,
|
|
26
26
|
"session" => session,
|
|
27
27
|
"serializer" => serializer,
|
|
28
|
-
"persistence" => session
|
|
28
|
+
"persistence" => session,
|
|
29
|
+
"tools" => tools_section,
|
|
30
|
+
"search_tools" => tools_section,
|
|
31
|
+
"load_tool" => tools_section,
|
|
32
|
+
"forge" => tools_section
|
|
29
33
|
}
|
|
30
34
|
end
|
|
31
35
|
|
|
@@ -69,13 +73,43 @@ module Claw
|
|
|
69
73
|
TEXT
|
|
70
74
|
end
|
|
71
75
|
|
|
76
|
+
def tools_section
|
|
77
|
+
<<~TEXT
|
|
78
|
+
Tool system in Claw (three layers):
|
|
79
|
+
|
|
80
|
+
1. Core tools (always loaded): read_var, write_var, call_func, eval, etc. + remember
|
|
81
|
+
2. Project tools (on-demand): .ruby-claw/tools/*.rb — indexed at startup, loaded via load_tool
|
|
82
|
+
3. Hub tools (remote): community tools from ruby-claw-toolhub, downloaded on demand
|
|
83
|
+
|
|
84
|
+
Key tools for discovery:
|
|
85
|
+
- search_tools: search available project/hub tools by keyword
|
|
86
|
+
- load_tool: load a discovered tool into the current session
|
|
87
|
+
|
|
88
|
+
Creating tools:
|
|
89
|
+
- Write a class including Claw::Tool with tool_name, description, parameter DSL
|
|
90
|
+
- Place in .ruby-claw/tools/
|
|
91
|
+
- Or use /forge to promote an eval-defined method to a formal tool
|
|
92
|
+
|
|
93
|
+
Tool class structure:
|
|
94
|
+
class MyTool
|
|
95
|
+
include Claw::Tool
|
|
96
|
+
tool_name "my_tool"
|
|
97
|
+
description "What it does"
|
|
98
|
+
parameter :input, type: "String", required: true, desc: "..."
|
|
99
|
+
def call(input:)
|
|
100
|
+
# logic
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
TEXT
|
|
104
|
+
end
|
|
105
|
+
|
|
72
106
|
def serializer
|
|
73
107
|
<<~TEXT
|
|
74
108
|
Runtime state serialization in Claw:
|
|
75
109
|
Claw::Serializer can save and restore local variables and method definitions.
|
|
76
110
|
- Claw::Serializer.save(binding, dir) — saves values.json + definitions.rb
|
|
77
111
|
- Claw::Serializer.restore(binding, dir) — restores from saved files
|
|
78
|
-
- Values:
|
|
112
|
+
- Values: MarshalMd.dump (human-readable Markdown) with JSON fallback
|
|
79
113
|
- Definitions: tracked via @__claw_definitions__ on the receiver
|
|
80
114
|
TEXT
|
|
81
115
|
end
|
data/lib/claw/memory_store.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Claw
|
|
|
7
7
|
# Replaces JSON memory/session files with human-readable Markdown.
|
|
8
8
|
#
|
|
9
9
|
# Directory layout:
|
|
10
|
-
# .
|
|
10
|
+
# .ruby-claw/
|
|
11
11
|
# MEMORY.md — long-term memory
|
|
12
12
|
# session.md — session summary
|
|
13
13
|
# values.json — kept as-is (Marshal data)
|
|
@@ -72,7 +72,7 @@ module Claw
|
|
|
72
72
|
private
|
|
73
73
|
|
|
74
74
|
def base_dir
|
|
75
|
-
@base_path || Mana.config.memory_path || File.join(Dir.pwd, ".
|
|
75
|
+
@base_path || Mana.config.memory_path || File.join(Dir.pwd, ".ruby-claw")
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
def memory_md_path
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Plan Mode: two-phase plan-then-execute workflow.
|
|
5
|
+
#
|
|
6
|
+
# Phase 1 (plan): LLM outputs a step-by-step plan without executing tools.
|
|
7
|
+
# Phase 2 (execute): After user confirmation, execute inside a fork for safety.
|
|
8
|
+
class PlanMode
|
|
9
|
+
STATES = %i[inactive ready planning reviewing executing].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :pending_plan, :state
|
|
12
|
+
|
|
13
|
+
def initialize(runtime)
|
|
14
|
+
@runtime = runtime
|
|
15
|
+
@state = :inactive
|
|
16
|
+
@pending_plan = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def active? = @state != :inactive
|
|
20
|
+
def pending? = @state == :reviewing
|
|
21
|
+
|
|
22
|
+
def toggle!
|
|
23
|
+
if @state == :inactive
|
|
24
|
+
@state = :ready # activated, waiting for plan! call
|
|
25
|
+
true
|
|
26
|
+
else
|
|
27
|
+
discard!
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Phase 1: Generate a plan (no tool execution).
|
|
33
|
+
#
|
|
34
|
+
# @param prompt [String] user's task description
|
|
35
|
+
# @param caller_binding [Binding] for binding context
|
|
36
|
+
# @param on_text [Proc, nil] streaming callback
|
|
37
|
+
# @return [String] the plan text
|
|
38
|
+
def plan!(prompt, caller_binding, &on_text)
|
|
39
|
+
@state = :planning
|
|
40
|
+
|
|
41
|
+
binding_md = @runtime&.resources&.dig("binding")&.to_md || "(no binding)"
|
|
42
|
+
memory_md = @runtime&.resources&.dig("memory")&.to_md || "(no memory)"
|
|
43
|
+
|
|
44
|
+
planning_prompt = <<~PROMPT
|
|
45
|
+
The user wants: #{prompt}
|
|
46
|
+
|
|
47
|
+
Current binding state:
|
|
48
|
+
#{binding_md}
|
|
49
|
+
|
|
50
|
+
Current memory:
|
|
51
|
+
#{memory_md}
|
|
52
|
+
|
|
53
|
+
Output ONLY a step-by-step plan describing what tools you would use and in what order.
|
|
54
|
+
Do NOT call any tools. Do NOT execute anything.
|
|
55
|
+
Format as a numbered list. For each step, specify:
|
|
56
|
+
- Which tool to use
|
|
57
|
+
- On what target (variable/method/etc.)
|
|
58
|
+
- Expected result
|
|
59
|
+
PROMPT
|
|
60
|
+
|
|
61
|
+
engine = Mana::Engine.new(caller_binding)
|
|
62
|
+
# Execute with empty tools array so LLM cannot call any tools
|
|
63
|
+
plan_text = engine.execute(planning_prompt, &on_text)
|
|
64
|
+
|
|
65
|
+
@pending_plan = {
|
|
66
|
+
prompt: prompt,
|
|
67
|
+
plan_text: plan_text.to_s,
|
|
68
|
+
created_at: Time.now
|
|
69
|
+
}
|
|
70
|
+
@state = :reviewing
|
|
71
|
+
@pending_plan[:plan_text]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Phase 2: Execute the approved plan inside a fork for safety.
|
|
75
|
+
#
|
|
76
|
+
# @param caller_binding [Binding]
|
|
77
|
+
# @param edited_plan [String, nil] user-edited plan text (optional)
|
|
78
|
+
# @param on_text [Proc, nil] streaming callback
|
|
79
|
+
# @return [Array] [success, result] from Runtime#fork
|
|
80
|
+
def execute!(caller_binding, edited_plan: nil, &on_text)
|
|
81
|
+
raise "No pending plan" unless @state == :reviewing
|
|
82
|
+
|
|
83
|
+
@state = :executing
|
|
84
|
+
plan_text = edited_plan || @pending_plan[:plan_text]
|
|
85
|
+
prompt = @pending_plan[:prompt]
|
|
86
|
+
@pending_plan = nil
|
|
87
|
+
|
|
88
|
+
result = @runtime.fork(label: "plan_execution") do
|
|
89
|
+
engine = Mana::Engine.new(caller_binding)
|
|
90
|
+
engine.execute(<<~EXEC, &on_text)
|
|
91
|
+
Execute this task: #{prompt}
|
|
92
|
+
|
|
93
|
+
Your approved plan:
|
|
94
|
+
#{plan_text}
|
|
95
|
+
|
|
96
|
+
Follow the plan step by step. Use the available tools to complete each step.
|
|
97
|
+
EXEC
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
@state = :inactive
|
|
101
|
+
result
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Discard the pending plan.
|
|
105
|
+
def discard!
|
|
106
|
+
@pending_plan = nil
|
|
107
|
+
@state = :inactive
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
# Interface for reversible resources managed by Claw::Runtime.
|
|
5
|
+
# All resources that participate in snapshot/rollback must include this module
|
|
6
|
+
# and implement the required methods.
|
|
7
|
+
module Resource
|
|
8
|
+
# Capture current state. Returns an opaque token for later rollback.
|
|
9
|
+
def snapshot!
|
|
10
|
+
raise NotImplementedError, "#{self.class}#snapshot! not implemented"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Restore state to a previous snapshot token.
|
|
14
|
+
# Implementations must guarantee success — partial rollback is not acceptable.
|
|
15
|
+
def rollback!(token)
|
|
16
|
+
raise NotImplementedError, "#{self.class}#rollback! not implemented"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Compare two snapshot tokens. Returns a human-readable diff string.
|
|
20
|
+
def diff(token_a, token_b)
|
|
21
|
+
raise NotImplementedError, "#{self.class}#diff not implemented"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Render current state as Markdown for human/LLM consumption.
|
|
25
|
+
def to_md
|
|
26
|
+
raise NotImplementedError, "#{self.class}#to_md not implemented"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Merge changes from another resource instance (e.g., child → parent).
|
|
30
|
+
# Used by V8 Multi-Agent to selectively merge child results back.
|
|
31
|
+
def merge_from!(other)
|
|
32
|
+
raise NotImplementedError, "#{self.class}#merge_from! not implemented"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|