looped 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/PLAN.md +856 -0
- data/README.md +340 -0
- data/docs/self-improving-coding-agent.md +374 -0
- data/exe/looped +115 -0
- data/lib/looped/agent.rb +188 -0
- data/lib/looped/application.rb +252 -0
- data/lib/looped/judge.rb +90 -0
- data/lib/looped/memory.rb +96 -0
- data/lib/looped/optimizer.rb +267 -0
- data/lib/looped/signatures.rb +40 -0
- data/lib/looped/state.rb +120 -0
- data/lib/looped/tools/read_file.rb +35 -0
- data/lib/looped/tools/run_command.rb +56 -0
- data/lib/looped/tools/search_code.rb +38 -0
- data/lib/looped/tools/write_file.rb +37 -0
- data/lib/looped/types.rb +53 -0
- data/lib/looped/version.rb +6 -0
- data/lib/looped.rb +100 -0
- data/looped.gemspec +47 -0
- metadata +246 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'dspy'
|
|
5
|
+
|
|
6
|
+
module Looped
|
|
7
|
+
# Main coding task signature
|
|
8
|
+
class CodingTaskSignature < DSPy::Signature
|
|
9
|
+
description "Complete a coding task in any programming language."
|
|
10
|
+
|
|
11
|
+
input do
|
|
12
|
+
const :task, String
|
|
13
|
+
const :context, String, default: ''
|
|
14
|
+
const :history, T::Array[Types::ActionSummary], default: []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
output do
|
|
18
|
+
const :solution, String
|
|
19
|
+
const :files_modified, T::Array[String], default: []
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# LLM-as-Judge signature
|
|
24
|
+
class JudgeSignature < DSPy::Signature
|
|
25
|
+
description "Evaluate code quality and correctness. Be critical and thorough."
|
|
26
|
+
|
|
27
|
+
input do
|
|
28
|
+
const :task, String
|
|
29
|
+
const :solution, String
|
|
30
|
+
const :expected_behavior, String
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
output do
|
|
34
|
+
const :score, Float
|
|
35
|
+
const :passed, T::Boolean
|
|
36
|
+
const :critique, String
|
|
37
|
+
const :suggestions, T::Array[String]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/looped/state.rb
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module Looped
|
|
8
|
+
class State
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
DEFAULT_STORAGE_DIR = T.let(File.expand_path('~/.looped'), String)
|
|
12
|
+
|
|
13
|
+
sig { returns(String) }
|
|
14
|
+
attr_reader :storage_dir
|
|
15
|
+
|
|
16
|
+
sig { params(storage_dir: T.nilable(String)).void }
|
|
17
|
+
def initialize(storage_dir: nil)
|
|
18
|
+
@storage_dir = T.let(
|
|
19
|
+
storage_dir || ENV['LOOPED_STORAGE_DIR'] || DEFAULT_STORAGE_DIR,
|
|
20
|
+
String
|
|
21
|
+
)
|
|
22
|
+
FileUtils.mkdir_p(@storage_dir)
|
|
23
|
+
FileUtils.mkdir_p(File.join(@storage_dir, 'history'))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(T.nilable(Types::Instructions)) }
|
|
27
|
+
def load_instructions
|
|
28
|
+
path = instructions_path
|
|
29
|
+
return nil unless File.exist?(path)
|
|
30
|
+
|
|
31
|
+
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
32
|
+
instructions_data = data[:instructions] || {}
|
|
33
|
+
|
|
34
|
+
Types::Instructions.new(
|
|
35
|
+
thought_generator: instructions_data[:thought_generator],
|
|
36
|
+
observation_processor: instructions_data[:observation_processor],
|
|
37
|
+
score: data[:score]&.to_f || 0.0,
|
|
38
|
+
generation: data[:generation]&.to_i || 0,
|
|
39
|
+
updated_at: data[:updated_at] || Time.now.utc.iso8601
|
|
40
|
+
)
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig { params(instructions: T::Hash[Symbol, T.nilable(String)], score: Float, generation: Integer).void }
|
|
46
|
+
def save_instructions(instructions:, score:, generation:)
|
|
47
|
+
data = {
|
|
48
|
+
instructions: instructions,
|
|
49
|
+
score: score,
|
|
50
|
+
generation: generation,
|
|
51
|
+
updated_at: Time.now.utc.iso8601
|
|
52
|
+
}
|
|
53
|
+
File.write(instructions_path, JSON.pretty_generate(data))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig { params(result: Types::TrainingResult).void }
|
|
57
|
+
def append_training_result(result)
|
|
58
|
+
buffer = load_training_buffer_raw
|
|
59
|
+
buffer << {
|
|
60
|
+
task: result.task,
|
|
61
|
+
solution: result.solution,
|
|
62
|
+
score: result.score,
|
|
63
|
+
feedback: result.feedback,
|
|
64
|
+
timestamp: result.timestamp
|
|
65
|
+
}
|
|
66
|
+
File.write(training_buffer_path, JSON.pretty_generate(buffer))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
sig { returns(T::Array[Types::TrainingResult]) }
|
|
70
|
+
def peek_training_buffer
|
|
71
|
+
load_training_buffer_raw.map { |data| deserialize_training_result(data) }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { returns(T::Array[Types::TrainingResult]) }
|
|
75
|
+
def consume_training_buffer
|
|
76
|
+
buffer = peek_training_buffer
|
|
77
|
+
return [] if buffer.empty?
|
|
78
|
+
|
|
79
|
+
# Archive to history
|
|
80
|
+
archive_path = File.join(@storage_dir, 'history', "#{Time.now.to_i}.json")
|
|
81
|
+
File.write(archive_path, JSON.pretty_generate(load_training_buffer_raw))
|
|
82
|
+
|
|
83
|
+
# Clear buffer
|
|
84
|
+
File.write(training_buffer_path, '[]')
|
|
85
|
+
|
|
86
|
+
buffer
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
sig { returns(String) }
|
|
92
|
+
def instructions_path
|
|
93
|
+
File.join(@storage_dir, 'instructions.json')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
sig { returns(String) }
|
|
97
|
+
def training_buffer_path
|
|
98
|
+
File.join(@storage_dir, 'training_buffer.json')
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
|
|
102
|
+
def load_training_buffer_raw
|
|
103
|
+
return [] unless File.exist?(training_buffer_path)
|
|
104
|
+
JSON.parse(File.read(training_buffer_path), symbolize_names: true)
|
|
105
|
+
rescue JSON::ParserError
|
|
106
|
+
[]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(data: T::Hash[Symbol, T.untyped]).returns(Types::TrainingResult) }
|
|
110
|
+
def deserialize_training_result(data)
|
|
111
|
+
Types::TrainingResult.new(
|
|
112
|
+
task: data[:task] || '',
|
|
113
|
+
solution: data[:solution] || '',
|
|
114
|
+
score: data[:score]&.to_f || 0.0,
|
|
115
|
+
feedback: data[:feedback] || '',
|
|
116
|
+
timestamp: data[:timestamp] || Time.now.utc.iso8601
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Looped
|
|
5
|
+
module Tools
|
|
6
|
+
class ReadFile < DSPy::Tools::Base
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
tool_name 'read_file'
|
|
10
|
+
tool_description 'Read contents of a file at the given path'
|
|
11
|
+
|
|
12
|
+
sig { params(path: String).returns(String) }
|
|
13
|
+
def call(path:)
|
|
14
|
+
resolved_path = resolve_path(path)
|
|
15
|
+
File.read(resolved_path)
|
|
16
|
+
rescue Errno::ENOENT
|
|
17
|
+
"Error: File not found: #{path}"
|
|
18
|
+
rescue Errno::EACCES
|
|
19
|
+
"Error: Permission denied: #{path}"
|
|
20
|
+
rescue StandardError => e
|
|
21
|
+
"Error: #{e.message}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
sig { params(path: String).returns(String) }
|
|
27
|
+
def resolve_path(path)
|
|
28
|
+
sandbox = ENV['LOOPED_SANDBOX_DIR']
|
|
29
|
+
return path if sandbox.nil? || path.start_with?('/')
|
|
30
|
+
|
|
31
|
+
File.join(sandbox, path)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Looped
|
|
7
|
+
module Tools
|
|
8
|
+
class RunCommand < DSPy::Tools::Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
DEFAULT_TIMEOUT = 30
|
|
12
|
+
|
|
13
|
+
tool_name 'run_command'
|
|
14
|
+
tool_description 'Execute a shell command and return its output. Use for running tests, linters, or other development tools.'
|
|
15
|
+
|
|
16
|
+
sig { params(command: String, timeout: Integer).returns(String) }
|
|
17
|
+
def call(command:, timeout: DEFAULT_TIMEOUT)
|
|
18
|
+
# TODO: Implement Docker sandbox via trusted-sandbox gem for production
|
|
19
|
+
pid = T.let(nil, T.nilable(Integer))
|
|
20
|
+
output = +''
|
|
21
|
+
|
|
22
|
+
begin
|
|
23
|
+
stdin, stdout_err, wait_thr = Open3.popen2e(command)
|
|
24
|
+
pid = wait_thr.pid
|
|
25
|
+
stdin.close
|
|
26
|
+
|
|
27
|
+
# Wait for process with timeout using a thread
|
|
28
|
+
reader = Thread.new { stdout_err.read }
|
|
29
|
+
|
|
30
|
+
if wait_thr.join(timeout)
|
|
31
|
+
# Process completed in time
|
|
32
|
+
output = reader.value
|
|
33
|
+
exit_status = wait_thr.value
|
|
34
|
+
"Exit code: #{exit_status.exitstatus}\n#{output}"
|
|
35
|
+
else
|
|
36
|
+
# Timeout - kill the process
|
|
37
|
+
reader.kill
|
|
38
|
+
Process.kill('TERM', pid)
|
|
39
|
+
sleep 0.1
|
|
40
|
+
begin
|
|
41
|
+
Process.kill('KILL', pid) if wait_thr.alive?
|
|
42
|
+
rescue Errno::ESRCH
|
|
43
|
+
# Process already dead
|
|
44
|
+
end
|
|
45
|
+
wait_thr.join
|
|
46
|
+
"Error: Command timed out after #{timeout} seconds"
|
|
47
|
+
end
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
"Error: #{e.message}"
|
|
50
|
+
ensure
|
|
51
|
+
stdout_err&.close rescue nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module Looped
|
|
7
|
+
module Tools
|
|
8
|
+
class SearchCode < DSPy::Tools::Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
tool_name 'search_code'
|
|
12
|
+
tool_description 'Search for a pattern in code files using ripgrep. Returns matching lines with file paths and line numbers.'
|
|
13
|
+
|
|
14
|
+
sig { params(pattern: String, path: String, file_type: T.nilable(String)).returns(String) }
|
|
15
|
+
def call(pattern:, path: '.', file_type: nil)
|
|
16
|
+
cmd = build_command(pattern, path, file_type)
|
|
17
|
+
output, status = Open3.capture2e(*cmd)
|
|
18
|
+
|
|
19
|
+
if status.success?
|
|
20
|
+
output.empty? ? "No matches found for: #{pattern}" : output
|
|
21
|
+
else
|
|
22
|
+
"No matches found for: #{pattern}"
|
|
23
|
+
end
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
"Error: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
sig { params(pattern: String, path: String, file_type: T.nilable(String)).returns(T::Array[String]) }
|
|
31
|
+
def build_command(pattern, path, file_type)
|
|
32
|
+
cmd = ['rg', '--line-number', '--no-heading', '--max-count', '50', pattern, path]
|
|
33
|
+
cmd += ['--type', file_type] if file_type
|
|
34
|
+
cmd
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Looped
|
|
7
|
+
module Tools
|
|
8
|
+
class WriteFile < DSPy::Tools::Base
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
tool_name 'write_file'
|
|
12
|
+
tool_description 'Write content to a file at the given path. Creates parent directories if needed.'
|
|
13
|
+
|
|
14
|
+
sig { params(path: String, content: String).returns(String) }
|
|
15
|
+
def call(path:, content:)
|
|
16
|
+
resolved_path = resolve_path(path)
|
|
17
|
+
FileUtils.mkdir_p(File.dirname(resolved_path))
|
|
18
|
+
File.write(resolved_path, content)
|
|
19
|
+
"Successfully wrote #{content.length} bytes to #{path}"
|
|
20
|
+
rescue Errno::EACCES
|
|
21
|
+
"Error: Permission denied: #{path}"
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
"Error: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
sig { params(path: String).returns(String) }
|
|
29
|
+
def resolve_path(path)
|
|
30
|
+
sandbox = ENV['LOOPED_SANDBOX_DIR']
|
|
31
|
+
return path if sandbox.nil? || path.start_with?('/')
|
|
32
|
+
|
|
33
|
+
File.join(sandbox, path)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/looped/types.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module Looped
|
|
7
|
+
module Types
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
# Rich memory entry for storage/analytics
|
|
11
|
+
class MemoryEntry < T::Struct
|
|
12
|
+
const :action_type, String
|
|
13
|
+
const :action_input, T::Hash[String, T.untyped]
|
|
14
|
+
const :action_output, String
|
|
15
|
+
const :timestamp, String
|
|
16
|
+
const :model_id, T.nilable(String)
|
|
17
|
+
const :error, T.nilable(String)
|
|
18
|
+
const :tokens_used, T.nilable(Integer)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Lean context entry for prompts
|
|
22
|
+
class ActionSummary < T::Struct
|
|
23
|
+
const :action, String
|
|
24
|
+
const :result, String
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Training result stored to buffer
|
|
28
|
+
class TrainingResult < T::Struct
|
|
29
|
+
const :task, String
|
|
30
|
+
const :solution, String
|
|
31
|
+
const :score, Float
|
|
32
|
+
const :feedback, String
|
|
33
|
+
const :timestamp, String
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Persisted instructions with metadata
|
|
37
|
+
class Instructions < T::Struct
|
|
38
|
+
const :thought_generator, T.nilable(String)
|
|
39
|
+
const :observation_processor, T.nilable(String)
|
|
40
|
+
const :score, Float, default: 0.0
|
|
41
|
+
const :generation, Integer, default: 0
|
|
42
|
+
const :updated_at, String
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Judgment from LLM-as-judge
|
|
46
|
+
class Judgment < T::Struct
|
|
47
|
+
const :score, Float
|
|
48
|
+
const :passed, T::Boolean
|
|
49
|
+
const :critique, String
|
|
50
|
+
const :suggestions, T::Array[String], default: []
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/looped.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require 'dspy'
|
|
6
|
+
require 'dspy/openai'
|
|
7
|
+
|
|
8
|
+
# Load GEPA extension
|
|
9
|
+
begin
|
|
10
|
+
require 'dspy/teleprompt/gepa'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# GEPA not available - optimization will be limited
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative 'looped/version'
|
|
16
|
+
require_relative 'looped/types'
|
|
17
|
+
require_relative 'looped/signatures'
|
|
18
|
+
require_relative 'looped/memory'
|
|
19
|
+
require_relative 'looped/state'
|
|
20
|
+
|
|
21
|
+
# Tools
|
|
22
|
+
require_relative 'looped/tools/read_file'
|
|
23
|
+
require_relative 'looped/tools/write_file'
|
|
24
|
+
require_relative 'looped/tools/search_code'
|
|
25
|
+
require_relative 'looped/tools/run_command'
|
|
26
|
+
|
|
27
|
+
require_relative 'looped/judge'
|
|
28
|
+
require_relative 'looped/agent'
|
|
29
|
+
require_relative 'looped/optimizer'
|
|
30
|
+
require_relative 'looped/application'
|
|
31
|
+
|
|
32
|
+
module Looped
|
|
33
|
+
extend T::Sig
|
|
34
|
+
|
|
35
|
+
class Error < StandardError; end
|
|
36
|
+
|
|
37
|
+
# Convenience method to create and run the application
|
|
38
|
+
sig do
|
|
39
|
+
params(
|
|
40
|
+
model: T.nilable(String),
|
|
41
|
+
judge_model: T.nilable(String),
|
|
42
|
+
reflection_model: T.nilable(String),
|
|
43
|
+
max_iterations: Integer,
|
|
44
|
+
optimizer_batch_size: Integer,
|
|
45
|
+
optimizer_interval: Integer
|
|
46
|
+
).void
|
|
47
|
+
end
|
|
48
|
+
def self.run(
|
|
49
|
+
model: nil,
|
|
50
|
+
judge_model: nil,
|
|
51
|
+
reflection_model: nil,
|
|
52
|
+
max_iterations: 10,
|
|
53
|
+
optimizer_batch_size: 5,
|
|
54
|
+
optimizer_interval: 60
|
|
55
|
+
)
|
|
56
|
+
app = Application.new(
|
|
57
|
+
model: model,
|
|
58
|
+
judge_model: judge_model,
|
|
59
|
+
reflection_model: reflection_model,
|
|
60
|
+
max_iterations: max_iterations,
|
|
61
|
+
optimizer_batch_size: optimizer_batch_size,
|
|
62
|
+
optimizer_interval: optimizer_interval
|
|
63
|
+
)
|
|
64
|
+
app.run
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Run a single task without the interactive loop
|
|
68
|
+
sig do
|
|
69
|
+
params(
|
|
70
|
+
task: String,
|
|
71
|
+
context: String,
|
|
72
|
+
model: T.nilable(String),
|
|
73
|
+
judge_model: T.nilable(String),
|
|
74
|
+
reflection_model: T.nilable(String),
|
|
75
|
+
max_iterations: Integer,
|
|
76
|
+
optimizer_batch_size: Integer,
|
|
77
|
+
optimizer_interval: Integer
|
|
78
|
+
).returns(Types::TrainingResult)
|
|
79
|
+
end
|
|
80
|
+
def self.execute(
|
|
81
|
+
task:,
|
|
82
|
+
context: '',
|
|
83
|
+
model: nil,
|
|
84
|
+
judge_model: nil,
|
|
85
|
+
reflection_model: nil,
|
|
86
|
+
max_iterations: 10,
|
|
87
|
+
optimizer_batch_size: 5,
|
|
88
|
+
optimizer_interval: 60
|
|
89
|
+
)
|
|
90
|
+
app = Application.new(
|
|
91
|
+
model: model,
|
|
92
|
+
judge_model: judge_model,
|
|
93
|
+
reflection_model: reflection_model,
|
|
94
|
+
max_iterations: max_iterations,
|
|
95
|
+
optimizer_batch_size: optimizer_batch_size,
|
|
96
|
+
optimizer_interval: optimizer_interval
|
|
97
|
+
)
|
|
98
|
+
app.run_task(task: task, context: context)
|
|
99
|
+
end
|
|
100
|
+
end
|
data/looped.gemspec
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/looped/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'looped'
|
|
7
|
+
spec.version = Looped::VERSION
|
|
8
|
+
spec.authors = ['Vicente Reig']
|
|
9
|
+
spec.email = ['hey@vicente.services']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'Self-improving coding agent with continuous prompt optimization'
|
|
12
|
+
spec.description = 'A coding agent that learns from its own performance using GEPA prompt optimization running in the background.'
|
|
13
|
+
spec.homepage = 'https://github.com/vicentereig/looped'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.1.0'
|
|
16
|
+
|
|
17
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
18
|
+
spec.metadata['source_code_uri'] = 'https://github.com/vicentereig/looped'
|
|
19
|
+
spec.metadata['changelog_uri'] = 'https://github.com/vicentereig/looped/blob/main/CHANGELOG.md'
|
|
20
|
+
|
|
21
|
+
spec.files = Dir.chdir(__dir__) do
|
|
22
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
|
23
|
+
(File.expand_path(f) == __FILE__) ||
|
|
24
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
spec.bindir = 'exe'
|
|
28
|
+
spec.executables = ['looped']
|
|
29
|
+
spec.require_paths = ['lib']
|
|
30
|
+
|
|
31
|
+
# Runtime dependencies
|
|
32
|
+
spec.add_dependency 'async', '~> 2.23'
|
|
33
|
+
spec.add_dependency 'dspy', '~> 0.31.1'
|
|
34
|
+
spec.add_dependency 'dspy-openai', '~> 1.0'
|
|
35
|
+
spec.add_dependency 'dspy-gepa', '~> 1.0.3'
|
|
36
|
+
spec.add_dependency 'gepa', '~> 1.0.2'
|
|
37
|
+
spec.add_dependency 'polars-df', '~> 0.23'
|
|
38
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
|
39
|
+
|
|
40
|
+
# Development dependencies
|
|
41
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
42
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
43
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
44
|
+
spec.add_development_dependency 'vcr', '~> 6.2'
|
|
45
|
+
spec.add_development_dependency 'webmock', '~> 3.18'
|
|
46
|
+
spec.add_development_dependency 'byebug', '~> 11.1'
|
|
47
|
+
end
|