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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sinatra/base"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Claw
|
|
7
|
+
module Console
|
|
8
|
+
# Local web server for agent observability and operations.
|
|
9
|
+
# Serves the console UI and provides API endpoints.
|
|
10
|
+
class Server < Sinatra::Base
|
|
11
|
+
set :views, File.join(__dir__, "views")
|
|
12
|
+
set :public_folder, File.join(__dir__, "public")
|
|
13
|
+
set :bind, "127.0.0.1"
|
|
14
|
+
set :port, 4567
|
|
15
|
+
set :server, :webrick
|
|
16
|
+
|
|
17
|
+
# Allow all hosts in development/testing (console is localhost-only)
|
|
18
|
+
set :host_authorization, { permitted_hosts: [] }
|
|
19
|
+
|
|
20
|
+
# Shared state — configured before starting
|
|
21
|
+
class << self
|
|
22
|
+
attr_accessor :event_logger, :runtime, :memory_instance, :claw_dir
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Configure the server with runtime references.
|
|
26
|
+
def self.setup(claw_dir:, runtime: nil, memory: nil, port: 4567)
|
|
27
|
+
self.claw_dir = claw_dir
|
|
28
|
+
self.runtime = runtime
|
|
29
|
+
self.memory_instance = memory
|
|
30
|
+
self.event_logger = EventLogger.new(File.join(claw_dir, "log"))
|
|
31
|
+
set :port, port
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# --- Pages ---
|
|
35
|
+
|
|
36
|
+
get "/" do
|
|
37
|
+
erb :index
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
get "/prompt" do
|
|
41
|
+
erb :prompt
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
get "/monitor" do
|
|
45
|
+
erb :monitor
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
get "/traces" do
|
|
49
|
+
erb :traces
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
get "/memory" do
|
|
53
|
+
erb :memory
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
get "/tools" do
|
|
57
|
+
erb :tools
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
get "/snapshots" do
|
|
61
|
+
erb :snapshots
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
get "/experiments" do
|
|
65
|
+
erb :experiments
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- API Endpoints ---
|
|
69
|
+
|
|
70
|
+
get "/api/status" do
|
|
71
|
+
content_type :json
|
|
72
|
+
{
|
|
73
|
+
version: Claw::VERSION,
|
|
74
|
+
state: self.class.runtime&.state,
|
|
75
|
+
snapshot_count: self.class.runtime&.snapshots&.size || 0,
|
|
76
|
+
memory_count: self.class.memory_instance&.long_term&.size || 0,
|
|
77
|
+
tool_count: Mana.registered_tools.size,
|
|
78
|
+
event_count: self.class.event_logger&.count || 0
|
|
79
|
+
}.to_json
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
get "/api/events" do
|
|
83
|
+
content_type "text/event-stream"
|
|
84
|
+
cache_control :no_cache
|
|
85
|
+
|
|
86
|
+
stream(:keep_open) do |out|
|
|
87
|
+
SSE.stream_events(out, self.class.event_logger)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
get "/api/traces" do
|
|
92
|
+
content_type :json
|
|
93
|
+
traces_dir = File.join(self.class.claw_dir, "traces")
|
|
94
|
+
unless Dir.exist?(traces_dir)
|
|
95
|
+
return [].to_json
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
files = Dir.glob(File.join(traces_dir, "*.md")).sort.reverse.first(50)
|
|
99
|
+
files.map do |f|
|
|
100
|
+
{ id: File.basename(f, ".md"), filename: File.basename(f),
|
|
101
|
+
size: File.size(f), modified: File.mtime(f).iso8601 }
|
|
102
|
+
end.to_json
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
get "/api/traces/:id" do
|
|
106
|
+
content_type :json
|
|
107
|
+
halt 400, { error: "Invalid trace ID" }.to_json unless params[:id] =~ /\A[a-zA-Z0-9_\-]+\z/
|
|
108
|
+
|
|
109
|
+
path = File.join(self.class.claw_dir, "traces", "#{params[:id]}.md")
|
|
110
|
+
halt 404, { error: "Trace not found" }.to_json unless File.exist?(path)
|
|
111
|
+
|
|
112
|
+
{ id: params[:id], content: File.read(path) }.to_json
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
get "/api/memory" do
|
|
116
|
+
content_type :json
|
|
117
|
+
mem = self.class.memory_instance
|
|
118
|
+
unless mem
|
|
119
|
+
return [].to_json
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
mem.long_term.to_json
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
get "/api/prompt" do
|
|
126
|
+
content_type :json
|
|
127
|
+
prompt_path = File.join(self.class.claw_dir, "system_prompt.md")
|
|
128
|
+
content = File.exist?(prompt_path) ? File.read(prompt_path) : ""
|
|
129
|
+
|
|
130
|
+
sections = Mana.instance_variable_get(:@prompt_sections)&.filter_map(&:call) || []
|
|
131
|
+
|
|
132
|
+
{ template: content, sections: sections }.to_json
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
get "/api/prompt/sections" do
|
|
136
|
+
content_type :json
|
|
137
|
+
sections = Mana.instance_variable_get(:@prompt_sections)&.filter_map(&:call) || []
|
|
138
|
+
sections.to_json
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
get "/api/tools" do
|
|
142
|
+
content_type :json
|
|
143
|
+
registry = Claw.tool_registry
|
|
144
|
+
core_tools = Mana.registered_tools.map { |t| { name: t[:name], description: t[:description], source: "core" } }
|
|
145
|
+
|
|
146
|
+
project_tools = registry ? registry.index.entries.map do |e|
|
|
147
|
+
{ name: e.name, description: e.description, source: "project",
|
|
148
|
+
loaded: registry.loaded?(e.name) }
|
|
149
|
+
end : []
|
|
150
|
+
|
|
151
|
+
{ core: core_tools, project: project_tools }.to_json
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# --- Helpers ---
|
|
155
|
+
|
|
156
|
+
helpers do
|
|
157
|
+
def parse_json!
|
|
158
|
+
data = JSON.parse(request.body.read, symbolize_names: true)
|
|
159
|
+
data
|
|
160
|
+
rescue JSON::ParserError
|
|
161
|
+
halt 400, { error: "Invalid JSON" }.to_json
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def require_field!(data, field)
|
|
165
|
+
halt 400, { error: "Missing field: #{field}" }.to_json unless data[field]
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# --- Mutation API ---
|
|
170
|
+
|
|
171
|
+
post "/api/memory" do
|
|
172
|
+
content_type :json
|
|
173
|
+
data = parse_json!
|
|
174
|
+
require_field!(data, :content)
|
|
175
|
+
mem = self.class.memory_instance
|
|
176
|
+
halt 400, { error: "Memory not available" }.to_json unless mem
|
|
177
|
+
|
|
178
|
+
entry = mem.remember(data[:content])
|
|
179
|
+
{ success: true, entry: entry }.to_json
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
delete "/api/memory/:id" do
|
|
183
|
+
content_type :json
|
|
184
|
+
halt 400, { error: "Invalid ID" }.to_json unless params[:id] =~ /\A\d+\z/
|
|
185
|
+
mem = self.class.memory_instance
|
|
186
|
+
halt 400, { error: "Memory not available" }.to_json unless mem
|
|
187
|
+
|
|
188
|
+
mem.forget(id: params[:id].to_i)
|
|
189
|
+
{ success: true }.to_json
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
post "/api/prompt" do
|
|
193
|
+
content_type :json
|
|
194
|
+
data = parse_json!
|
|
195
|
+
require_field!(data, :content)
|
|
196
|
+
path = File.join(self.class.claw_dir, "system_prompt.md")
|
|
197
|
+
File.write(path, data[:content])
|
|
198
|
+
{ success: true }.to_json
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
post "/api/tools/load" do
|
|
202
|
+
content_type :json
|
|
203
|
+
data = parse_json!
|
|
204
|
+
require_field!(data, :name)
|
|
205
|
+
registry = Claw.tool_registry
|
|
206
|
+
halt 400, { error: "Tool registry not available" }.to_json unless registry
|
|
207
|
+
|
|
208
|
+
msg = registry.load(data[:name])
|
|
209
|
+
{ success: true, message: msg }.to_json
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
post "/api/tools/unload" do
|
|
213
|
+
content_type :json
|
|
214
|
+
data = parse_json!
|
|
215
|
+
require_field!(data, :name)
|
|
216
|
+
registry = Claw.tool_registry
|
|
217
|
+
halt 400, { error: "Tool registry not available" }.to_json unless registry
|
|
218
|
+
|
|
219
|
+
msg = registry.unload(data[:name])
|
|
220
|
+
{ success: true, message: msg }.to_json
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
post "/api/snapshots" do
|
|
224
|
+
content_type :json
|
|
225
|
+
runtime = self.class.runtime
|
|
226
|
+
halt 400, { error: "Runtime not available" }.to_json unless runtime
|
|
227
|
+
|
|
228
|
+
id = runtime.snapshot!(label: "console")
|
|
229
|
+
{ success: true, id: id }.to_json
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
post "/api/snapshots/:id/rollback" do
|
|
233
|
+
content_type :json
|
|
234
|
+
halt 400, { error: "Invalid ID" }.to_json unless params[:id] =~ /\A\d+\z/
|
|
235
|
+
runtime = self.class.runtime
|
|
236
|
+
halt 400, { error: "Runtime not available" }.to_json unless runtime
|
|
237
|
+
|
|
238
|
+
runtime.rollback!(params[:id].to_i)
|
|
239
|
+
{ success: true }.to_json
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
get "/api/snapshots" do
|
|
243
|
+
content_type :json
|
|
244
|
+
runtime = self.class.runtime
|
|
245
|
+
return [].to_json unless runtime
|
|
246
|
+
|
|
247
|
+
runtime.snapshots.map do |s|
|
|
248
|
+
{ id: s.id, label: s.label, timestamp: s.timestamp }
|
|
249
|
+
end.to_json
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Claw
|
|
4
|
+
module Console
|
|
5
|
+
# Server-Sent Events helper for streaming events to the browser.
|
|
6
|
+
module SSE
|
|
7
|
+
# Stream events from the event logger to a Sinatra stream block.
|
|
8
|
+
#
|
|
9
|
+
# @param stream [Object] Sinatra stream object (responds to <<)
|
|
10
|
+
# @param logger [EventLogger] the event source
|
|
11
|
+
# @param poll_interval [Float] seconds between polls
|
|
12
|
+
def self.stream_events(stream, logger, poll_interval: 0.5)
|
|
13
|
+
last_timestamp = nil
|
|
14
|
+
|
|
15
|
+
loop do
|
|
16
|
+
events = logger.tail(since: last_timestamp)
|
|
17
|
+
events.each do |event|
|
|
18
|
+
stream << "data: #{JSON.generate(event)}\n\n"
|
|
19
|
+
last_timestamp = event[:timestamp]
|
|
20
|
+
end
|
|
21
|
+
sleep(poll_interval)
|
|
22
|
+
end
|
|
23
|
+
rescue IOError
|
|
24
|
+
# Client disconnected
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<h1>Experiments</h1>
|
|
2
|
+
<p class="muted">Experiment platform allows forking runtime state, running prompt variations, and comparing results.</p>
|
|
3
|
+
<div class="experiment-controls">
|
|
4
|
+
<button class="btn" id="new-experiment" onclick="newExperiment()">New Experiment</button>
|
|
5
|
+
</div>
|
|
6
|
+
<div id="experiments-list">
|
|
7
|
+
<p class="muted">No experiments yet.</p>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<h1>Dashboard</h1>
|
|
2
|
+
<div class="cards" id="dashboard-cards">
|
|
3
|
+
<div class="card">
|
|
4
|
+
<div class="card-label">Version</div>
|
|
5
|
+
<div class="card-value" id="stat-version">—</div>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-label">Tools</div>
|
|
9
|
+
<div class="card-value" id="stat-tools">—</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="card">
|
|
12
|
+
<div class="card-label">Memories</div>
|
|
13
|
+
<div class="card-value" id="stat-memories">—</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card">
|
|
16
|
+
<div class="card-label">Snapshots</div>
|
|
17
|
+
<div class="card-value" id="stat-snapshots">—</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card">
|
|
20
|
+
<div class="card-label">Events</div>
|
|
21
|
+
<div class="card-value" id="stat-events">—</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<h2>Recent Events</h2>
|
|
25
|
+
<div class="event-feed" id="dashboard-events">
|
|
26
|
+
<p class="muted">No events yet.</p>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Claw Console</title>
|
|
7
|
+
<link rel="stylesheet" href="/style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header>
|
|
11
|
+
<div class="logo">claw</div>
|
|
12
|
+
<nav>
|
|
13
|
+
<a href="/" class="nav-link">Dashboard</a>
|
|
14
|
+
<a href="/prompt" class="nav-link">Prompt</a>
|
|
15
|
+
<a href="/monitor" class="nav-link">Monitor</a>
|
|
16
|
+
<a href="/traces" class="nav-link">Traces</a>
|
|
17
|
+
<a href="/memory" class="nav-link">Memory</a>
|
|
18
|
+
<a href="/tools" class="nav-link">Tools</a>
|
|
19
|
+
<a href="/snapshots" class="nav-link">Snapshots</a>
|
|
20
|
+
<a href="/experiments" class="nav-link">Experiments</a>
|
|
21
|
+
</nav>
|
|
22
|
+
<div class="header-status" id="header-status"></div>
|
|
23
|
+
</header>
|
|
24
|
+
<main>
|
|
25
|
+
<%= yield %>
|
|
26
|
+
</main>
|
|
27
|
+
<script src="/app.js"></script>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<h1>Memory</h1>
|
|
2
|
+
<div class="memory-controls">
|
|
3
|
+
<input type="text" id="memory-input" placeholder="Add a new memory..." class="text-input">
|
|
4
|
+
<button class="btn" onclick="addMemory()">Remember</button>
|
|
5
|
+
</div>
|
|
6
|
+
<table class="data-table" id="memory-table">
|
|
7
|
+
<thead>
|
|
8
|
+
<tr><th>ID</th><th>Content</th><th>Created</th><th></th></tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody id="memory-body">
|
|
11
|
+
<tr><td colspan="4" class="muted">Loading...</td></tr>
|
|
12
|
+
</tbody>
|
|
13
|
+
</table>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<h1>LLM Monitor</h1>
|
|
2
|
+
<div class="monitor-controls">
|
|
3
|
+
<label><input type="checkbox" id="auto-scroll" checked> Auto-scroll</label>
|
|
4
|
+
<select id="event-filter">
|
|
5
|
+
<option value="">All events</option>
|
|
6
|
+
<option value="llm_call">LLM calls</option>
|
|
7
|
+
<option value="tool_call">Tool calls</option>
|
|
8
|
+
<option value="snapshot">Snapshots</option>
|
|
9
|
+
<option value="trace">Traces</option>
|
|
10
|
+
</select>
|
|
11
|
+
<span class="muted" id="event-count">0 events</span>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="event-stream" id="event-stream">
|
|
14
|
+
<p class="muted">Connecting to event stream...</p>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<h1>Prompt Inspector</h1>
|
|
2
|
+
<div class="prompt-section">
|
|
3
|
+
<h2>System Prompt Template</h2>
|
|
4
|
+
<div class="prompt-editor">
|
|
5
|
+
<textarea id="prompt-template" rows="15" spellcheck="false"></textarea>
|
|
6
|
+
<button class="btn" id="save-prompt" onclick="savePrompt()">Save</button>
|
|
7
|
+
<span id="prompt-status" class="muted"></span>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="prompt-section">
|
|
11
|
+
<h2>Dynamic Sections</h2>
|
|
12
|
+
<div id="prompt-sections" class="section-list">
|
|
13
|
+
<p class="muted">Loading...</p>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<h1>Snapshots</h1>
|
|
2
|
+
<div class="snapshot-controls">
|
|
3
|
+
<button class="btn" onclick="createSnapshot()">Take Snapshot</button>
|
|
4
|
+
</div>
|
|
5
|
+
<table class="data-table" id="snapshot-table">
|
|
6
|
+
<thead>
|
|
7
|
+
<tr><th>ID</th><th>Label</th><th>Timestamp</th><th></th></tr>
|
|
8
|
+
</thead>
|
|
9
|
+
<tbody id="snapshot-body">
|
|
10
|
+
<tr><td colspan="4" class="muted">Loading...</td></tr>
|
|
11
|
+
</tbody>
|
|
12
|
+
</table>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<h1>Tools</h1>
|
|
2
|
+
<div class="tools-section">
|
|
3
|
+
<h2>Core Tools</h2>
|
|
4
|
+
<div id="core-tools" class="tool-grid">
|
|
5
|
+
<p class="muted">Loading...</p>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="tools-section">
|
|
9
|
+
<h2>Project Tools</h2>
|
|
10
|
+
<div id="project-tools" class="tool-grid">
|
|
11
|
+
<p class="muted">Loading...</p>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<h1>Trace Explorer</h1>
|
|
2
|
+
<div class="trace-layout">
|
|
3
|
+
<div class="trace-list" id="trace-list">
|
|
4
|
+
<p class="muted">Loading traces...</p>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="trace-detail" id="trace-detail">
|
|
7
|
+
<p class="muted">Select a trace to view its content.</p>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
data/lib/claw/console.rb
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Claw
|
|
7
|
+
# Self-evolution loop: reads execution traces, uses LLM to diagnose
|
|
8
|
+
# improvements, forks runtime to apply changes, scores via test suite,
|
|
9
|
+
# and keeps or discards the change atomically.
|
|
10
|
+
#
|
|
11
|
+
# Depends on:
|
|
12
|
+
# - v3 Runtime (fork/rollback)
|
|
13
|
+
# - v5.1 Traces (.ruby-claw/traces/)
|
|
14
|
+
# - v5.2 claw init (.ruby-claw/gems/ editable source)
|
|
15
|
+
class Evolution
|
|
16
|
+
class RejectError < StandardError; end
|
|
17
|
+
|
|
18
|
+
DIAGNOSIS_SYSTEM = "You are a code improvement agent. Analyze execution traces and propose precise code changes. Respond only with valid JSON."
|
|
19
|
+
|
|
20
|
+
DIAGNOSIS_PROMPT = <<~PROMPT
|
|
21
|
+
Review these execution traces from a Ruby agent framework and propose ONE specific code change that would improve:
|
|
22
|
+
- Response quality (better tool use, fewer iterations)
|
|
23
|
+
- Performance (fewer tokens, lower latency)
|
|
24
|
+
- Robustness (better error handling, edge cases)
|
|
25
|
+
|
|
26
|
+
Respond with a JSON object:
|
|
27
|
+
{
|
|
28
|
+
"summary": "Brief description of the change",
|
|
29
|
+
"gem": "ruby-claw or ruby-mana",
|
|
30
|
+
"file": "relative/path/to/file.rb",
|
|
31
|
+
"old_code": "exact existing code to replace (copy-paste from source)",
|
|
32
|
+
"new_code": "replacement code",
|
|
33
|
+
"rationale": "why this improves the agent"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
If no meaningful improvements can be made, respond with:
|
|
37
|
+
{"summary": "no changes needed"}
|
|
38
|
+
|
|
39
|
+
IMPORTANT: old_code must be an exact substring of the file. Be precise.
|
|
40
|
+
PROMPT
|
|
41
|
+
|
|
42
|
+
attr_reader :results
|
|
43
|
+
|
|
44
|
+
# @param runtime [Claw::Runtime] the reversible runtime
|
|
45
|
+
# @param claw_dir [String] path to .ruby-claw/
|
|
46
|
+
# @param config [Mana::Config] LLM configuration
|
|
47
|
+
def initialize(runtime:, claw_dir:, config: Mana.config)
|
|
48
|
+
@runtime = runtime
|
|
49
|
+
@claw_dir = claw_dir
|
|
50
|
+
@gems_dir = File.join(claw_dir, "gems")
|
|
51
|
+
@config = config
|
|
52
|
+
@results = []
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Run one evolution cycle: diagnose → propose → test → keep/discard.
|
|
56
|
+
# Returns a result hash with :status, :proposal, :reason.
|
|
57
|
+
def evolve
|
|
58
|
+
traces = load_recent_traces
|
|
59
|
+
if traces.empty?
|
|
60
|
+
return log_result(status: :skip, reason: "no traces found")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
unless Dir.exist?(@gems_dir)
|
|
64
|
+
return log_result(status: :skip, reason: "no gems/ directory — run `claw init` first")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
proposal = diagnose(traces)
|
|
68
|
+
if proposal[:file].nil?
|
|
69
|
+
return log_result(status: :skip, reason: proposal[:summary])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
try_proposal(proposal)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Load recent trace files as strings.
|
|
76
|
+
def load_recent_traces(limit: 5)
|
|
77
|
+
dir = File.join(@claw_dir, "traces")
|
|
78
|
+
return [] unless Dir.exist?(dir)
|
|
79
|
+
|
|
80
|
+
Dir.glob(File.join(dir, "*.md"))
|
|
81
|
+
.sort_by { |f| File.mtime(f) }
|
|
82
|
+
.last(limit)
|
|
83
|
+
.map { |f| File.read(f) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Send traces to LLM for diagnosis. Returns a proposal hash.
|
|
87
|
+
def diagnose(traces)
|
|
88
|
+
prompt = DIAGNOSIS_PROMPT + "\n\n## Recent Traces\n\n" + traces.join("\n\n---\n\n")
|
|
89
|
+
|
|
90
|
+
backend = Mana::Backends::Base.for(@config)
|
|
91
|
+
response = backend.chat(
|
|
92
|
+
system: DIAGNOSIS_SYSTEM,
|
|
93
|
+
messages: [{ role: "user", content: prompt }],
|
|
94
|
+
tools: [],
|
|
95
|
+
model: @config.model
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
text = extract_text(response[:content])
|
|
99
|
+
parse_proposal(text)
|
|
100
|
+
rescue => e
|
|
101
|
+
{ summary: "diagnosis failed: #{e.message}" }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Attempt to apply a proposal inside a runtime fork.
|
|
105
|
+
def try_proposal(proposal)
|
|
106
|
+
gem_name = proposal[:gem] || "ruby-claw"
|
|
107
|
+
file_path = File.join(@gems_dir, gem_name, proposal[:file])
|
|
108
|
+
|
|
109
|
+
unless File.exist?(file_path)
|
|
110
|
+
return log_result(status: :reject, proposal: proposal[:summary],
|
|
111
|
+
reason: "file not found: #{proposal[:file]}")
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
content = File.read(file_path)
|
|
115
|
+
unless content.include?(proposal[:old_code])
|
|
116
|
+
return log_result(status: :reject, proposal: proposal[:summary],
|
|
117
|
+
reason: "old_code not found in #{proposal[:file]}")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
success, result = @runtime.fork(label: "evolve: #{proposal[:summary]}") do
|
|
121
|
+
# Apply the change
|
|
122
|
+
modified = content.sub(proposal[:old_code], proposal[:new_code])
|
|
123
|
+
File.write(file_path, modified)
|
|
124
|
+
|
|
125
|
+
# Score: run tests
|
|
126
|
+
score = run_tests(gem_name)
|
|
127
|
+
unless score[:passed]
|
|
128
|
+
raise RejectError, "tests failed:\n#{score[:output].to_s[0, 500]}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
score
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if success
|
|
135
|
+
# Write evolution log
|
|
136
|
+
write_evolution_log(proposal, :accept, result)
|
|
137
|
+
log_result(status: :accept, proposal: proposal[:summary],
|
|
138
|
+
rationale: proposal[:rationale])
|
|
139
|
+
else
|
|
140
|
+
write_evolution_log(proposal, :reject, result)
|
|
141
|
+
log_result(status: :reject, proposal: proposal[:summary],
|
|
142
|
+
reason: result.is_a?(Exception) ? result.message : result.to_s)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def extract_text(content)
|
|
149
|
+
return content.to_s unless content.is_a?(Array)
|
|
150
|
+
content.filter_map { |b| b[:text] || b["text"] }.join
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse_proposal(text)
|
|
154
|
+
json_match = text.match(/\{[\s\S]*\}/)
|
|
155
|
+
return { summary: "no JSON in response" } unless json_match
|
|
156
|
+
|
|
157
|
+
parsed = JSON.parse(json_match[0], symbolize_names: true)
|
|
158
|
+
parsed
|
|
159
|
+
rescue JSON::ParserError
|
|
160
|
+
{ summary: "failed to parse proposal JSON" }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def run_tests(gem_name)
|
|
164
|
+
gem_dir = File.join(@gems_dir, gem_name)
|
|
165
|
+
return { passed: true, output: "no gem directory" } unless Dir.exist?(gem_dir)
|
|
166
|
+
|
|
167
|
+
# Check if rspec is available
|
|
168
|
+
gemfile = File.join(gem_dir, "Gemfile")
|
|
169
|
+
unless File.exist?(gemfile)
|
|
170
|
+
return { passed: true, output: "no Gemfile — skipping tests" }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
out, status = Open3.capture2e(
|
|
174
|
+
"bundle", "exec", "rspec", "--format", "progress",
|
|
175
|
+
chdir: gem_dir
|
|
176
|
+
)
|
|
177
|
+
{ passed: status.success?, output: out }
|
|
178
|
+
rescue Errno::ENOENT
|
|
179
|
+
# bundle/rspec not found
|
|
180
|
+
{ passed: true, output: "rspec not available — skipping" }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def write_evolution_log(proposal, status, result)
|
|
184
|
+
log_dir = File.join(@claw_dir, "evolution")
|
|
185
|
+
FileUtils.mkdir_p(log_dir)
|
|
186
|
+
|
|
187
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
188
|
+
path = File.join(log_dir, "#{timestamp}_#{status}.md")
|
|
189
|
+
|
|
190
|
+
lines = []
|
|
191
|
+
lines << "# Evolution: #{proposal[:summary]}"
|
|
192
|
+
lines << ""
|
|
193
|
+
lines << "- Status: #{status}"
|
|
194
|
+
lines << "- Gem: #{proposal[:gem]}"
|
|
195
|
+
lines << "- File: #{proposal[:file]}"
|
|
196
|
+
lines << "- Rationale: #{proposal[:rationale]}"
|
|
197
|
+
lines << "- Timestamp: #{Time.now.iso8601}"
|
|
198
|
+
lines << ""
|
|
199
|
+
lines << "## Old Code"
|
|
200
|
+
lines << "```ruby"
|
|
201
|
+
lines << proposal[:old_code].to_s
|
|
202
|
+
lines << "```"
|
|
203
|
+
lines << ""
|
|
204
|
+
lines << "## New Code"
|
|
205
|
+
lines << "```ruby"
|
|
206
|
+
lines << proposal[:new_code].to_s
|
|
207
|
+
lines << "```"
|
|
208
|
+
|
|
209
|
+
if result.is_a?(Hash) && result[:output]
|
|
210
|
+
lines << ""
|
|
211
|
+
lines << "## Test Output"
|
|
212
|
+
lines << "```"
|
|
213
|
+
lines << result[:output].to_s[0, 2000]
|
|
214
|
+
lines << "```"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
File.write(path, lines.join("\n"))
|
|
218
|
+
rescue => e
|
|
219
|
+
# Don't crash on log failure
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def log_result(result)
|
|
223
|
+
@results << result
|
|
224
|
+
result
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|