self_agency 0.0.1
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/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +40 -0
- data/.irbrc +22 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +8 -0
- data/docs/api/configuration.md +85 -0
- data/docs/api/errors.md +166 -0
- data/docs/api/index.md +37 -0
- data/docs/api/self-agency-module.md +198 -0
- data/docs/architecture/overview.md +181 -0
- data/docs/architecture/security.md +101 -0
- data/docs/assets/images/self_agency.gif +0 -0
- data/docs/assets/images/self_agency.mp4 +0 -0
- data/docs/development/contributing.md +45 -0
- data/docs/development/setup.md +81 -0
- data/docs/development/testing.md +70 -0
- data/docs/examples/autonomous-robots.md +109 -0
- data/docs/examples/basic-examples.md +237 -0
- data/docs/examples/collaborative-robots.md +98 -0
- data/docs/examples/full-workflow.md +100 -0
- data/docs/examples/index.md +36 -0
- data/docs/getting-started/installation.md +71 -0
- data/docs/getting-started/quick-start.md +94 -0
- data/docs/guide/configuration.md +113 -0
- data/docs/guide/generating-methods.md +146 -0
- data/docs/guide/how-to-use.md +144 -0
- data/docs/guide/lifecycle-hooks.md +86 -0
- data/docs/guide/prompt-templates.md +189 -0
- data/docs/guide/saving-methods.md +84 -0
- data/docs/guide/scopes.md +74 -0
- data/docs/guide/source-inspection.md +96 -0
- data/docs/index.md +77 -0
- data/examples/01_basic_usage.rb +27 -0
- data/examples/02_multiple_methods.rb +43 -0
- data/examples/03_scopes.rb +40 -0
- data/examples/04_source_inspection.rb +46 -0
- data/examples/05_lifecycle_hook.rb +55 -0
- data/examples/06_configuration.rb +97 -0
- data/examples/07_error_handling.rb +103 -0
- data/examples/08_class_context.rb +64 -0
- data/examples/09_method_override.rb +52 -0
- data/examples/10_full_workflow.rb +118 -0
- data/examples/11_collaborative_robots/atlas.rb +31 -0
- data/examples/11_collaborative_robots/echo.rb +30 -0
- data/examples/11_collaborative_robots/main.rb +190 -0
- data/examples/11_collaborative_robots/nova.rb +71 -0
- data/examples/11_collaborative_robots/robot.rb +119 -0
- data/examples/12_autonomous_robots/analyst.rb +193 -0
- data/examples/12_autonomous_robots/collector.rb +78 -0
- data/examples/12_autonomous_robots/main.rb +166 -0
- data/examples/12_autonomous_robots/planner.rb +125 -0
- data/examples/12_autonomous_robots/robot.rb +284 -0
- data/examples/generated/from_range_class.rb +3 -0
- data/examples/generated/mean_instance.rb +4 -0
- data/examples/generated/median_instance.rb +15 -0
- data/examples/generated/report_singleton.rb +3 -0
- data/examples/generated/standard_deviation_instance.rb +8 -0
- data/examples/lib/message_bus.rb +57 -0
- data/examples/lib/setup.rb +8 -0
- data/lib/self_agency/configuration.rb +76 -0
- data/lib/self_agency/errors.rb +35 -0
- data/lib/self_agency/generator.rb +47 -0
- data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
- data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
- data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
- data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
- data/lib/self_agency/sandbox.rb +17 -0
- data/lib/self_agency/saver.rb +62 -0
- data/lib/self_agency/validator.rb +64 -0
- data/lib/self_agency/version.rb +5 -0
- data/lib/self_agency.rb +315 -0
- data/mkdocs.yml +156 -0
- data/sig/self_agency.rbs +4 -0
- metadata +163 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
require "ruby_llm/template"
|
|
5
|
+
|
|
6
|
+
module SelfAgency
|
|
7
|
+
CONFIG_MUTEX = Mutex.new
|
|
8
|
+
|
|
9
|
+
class Configuration
|
|
10
|
+
attr_accessor :provider, :model, :api_base,
|
|
11
|
+
:request_timeout, :max_retries, :retry_interval,
|
|
12
|
+
:template_directory, :generation_retries, :logger
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@provider = :ollama
|
|
16
|
+
@model = "qwen3-coder:30b"
|
|
17
|
+
@api_base = "http://localhost:11434/v1"
|
|
18
|
+
@request_timeout = 30
|
|
19
|
+
@max_retries = 1
|
|
20
|
+
@retry_interval = 0.5
|
|
21
|
+
@template_directory = File.join(__dir__, "prompts")
|
|
22
|
+
@generation_retries = 3
|
|
23
|
+
@logger = nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def configuration
|
|
29
|
+
@configuration ||= Configuration.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configure
|
|
33
|
+
CONFIG_MUTEX.synchronize do
|
|
34
|
+
yield(configuration)
|
|
35
|
+
apply_ruby_llm_config!
|
|
36
|
+
configuration
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reset!
|
|
41
|
+
CONFIG_MUTEX.synchronize do
|
|
42
|
+
@configuration = Configuration.new
|
|
43
|
+
@configured = false
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ensure_configured!
|
|
48
|
+
raise Error, "SelfAgency.configure has not been called" unless @configured
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def included(base)
|
|
52
|
+
base.extend(ClassMethods)
|
|
53
|
+
base.instance_variable_set(:@self_agency_mutex, Mutex.new)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def apply_ruby_llm_config!
|
|
59
|
+
cfg = configuration
|
|
60
|
+
provider_key = :"#{cfg.provider}_api_base"
|
|
61
|
+
|
|
62
|
+
RubyLLM.configure do |c|
|
|
63
|
+
c.public_send(:"#{provider_key}=", cfg.api_base) if c.respond_to?(:"#{provider_key}=")
|
|
64
|
+
c.request_timeout = cfg.request_timeout
|
|
65
|
+
c.max_retries = cfg.max_retries
|
|
66
|
+
c.retry_interval = cfg.retry_interval
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
RubyLLM::Template.configure do |c|
|
|
70
|
+
c.template_directory = cfg.template_directory
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@configured = true
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SelfAgency
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class GenerationError < Error
|
|
7
|
+
attr_reader :stage, :attempt
|
|
8
|
+
|
|
9
|
+
def initialize(message = nil, stage: nil, attempt: nil)
|
|
10
|
+
@stage = stage
|
|
11
|
+
@attempt = attempt
|
|
12
|
+
super(message)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class ValidationError < Error
|
|
17
|
+
attr_reader :generated_code, :attempt
|
|
18
|
+
|
|
19
|
+
def initialize(message = nil, generated_code: nil, attempt: nil)
|
|
20
|
+
@generated_code = generated_code
|
|
21
|
+
@attempt = attempt
|
|
22
|
+
super(message)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class SecurityError < Error
|
|
27
|
+
attr_reader :matched_pattern, :generated_code
|
|
28
|
+
|
|
29
|
+
def initialize(message = nil, matched_pattern: nil, generated_code: nil)
|
|
30
|
+
@matched_pattern = matched_pattern
|
|
31
|
+
@generated_code = generated_code
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SelfAgency
|
|
4
|
+
private
|
|
5
|
+
|
|
6
|
+
# Send a prompt to the configured LLM using a named template.
|
|
7
|
+
# Returns the response content string, or nil if the LLM returns empty content.
|
|
8
|
+
# Raises GenerationError wrapping the original exception on communication failure.
|
|
9
|
+
def self_agency_ask_with_template(template_name, **variables)
|
|
10
|
+
cfg = SelfAgency.configuration
|
|
11
|
+
chat = RubyLLM.chat(model: cfg.model, provider: cfg.provider)
|
|
12
|
+
response = chat.with_template(template_name, variables).complete
|
|
13
|
+
response.content
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
raise GenerationError.new(
|
|
16
|
+
"LLM request failed (#{e.class}: #{e.message})",
|
|
17
|
+
stage: template_name.to_sym
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Pass 1: rewrite the user's casual prompt into a precise technical spec.
|
|
22
|
+
def self_agency_shape(raw_prompt, scope)
|
|
23
|
+
scope_instruction = case scope
|
|
24
|
+
when :instance then "This will be an instance method available on all instances of the class."
|
|
25
|
+
when :singleton then "This will be a singleton method on one specific object instance only."
|
|
26
|
+
when :class then "This will be a class method (def self.method_name)."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
self_agency_ask_with_template(
|
|
30
|
+
:shape,
|
|
31
|
+
class_name: self.class.name,
|
|
32
|
+
ivars: instance_variables.join(", "),
|
|
33
|
+
methods: (self.class.public_instance_methods(false) - Object.public_instance_methods).sort.join(", "),
|
|
34
|
+
scope_instruction: scope_instruction,
|
|
35
|
+
raw_prompt: raw_prompt
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Build a Hash of introspected class context for the generate template.
|
|
40
|
+
def self_agency_generation_vars
|
|
41
|
+
{
|
|
42
|
+
class_name: self.class.name,
|
|
43
|
+
ivars: instance_variables.join(", "),
|
|
44
|
+
methods: (self.class.public_instance_methods(false) - Object.public_instance_methods).sort.join(", ")
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
You are a Ruby code generator. You MUST respond with ONLY a Ruby method
|
|
2
|
+
definition — nothing else. No explanation, no markdown fences, no comments
|
|
3
|
+
outside the method, no extra text.
|
|
4
|
+
|
|
5
|
+
Context for the class you are writing a method for:
|
|
6
|
+
- Class name: <%= class_name %>
|
|
7
|
+
- Instance variables: <%= ivars %>
|
|
8
|
+
- Public methods: <%= methods %>
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Return exactly one `def method_name ... end` block.
|
|
12
|
+
- Use the EXACT method name, parameter names, and Hash key names from the specification. Do NOT rename or abbreviate any identifier.
|
|
13
|
+
- Do NOT use any of these forbidden patterns: system, exec, spawn, fork, abort, exit, backticks, %x, File., IO., Kernel., Open3., Process., require, load, __send__, eval, send, public_send, method(), const_get, class_eval, module_eval, instance_eval, instance_variable_set, instance_variable_get, define_method, Binding, BasicObject, remove_method, undef_method.
|
|
14
|
+
- Do NOT wrap the code in markdown fences.
|
|
15
|
+
- The method must be self-contained.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
You are a prompt engineer specializing in Ruby code generation.
|
|
2
|
+
Your job is to take a casual, natural-language request and rewrite it
|
|
3
|
+
into a precise, unambiguous technical specification for a Ruby method.
|
|
4
|
+
|
|
5
|
+
Rules for the rewritten prompt:
|
|
6
|
+
- State the exact method name (snake_case). If the user didn't name one,
|
|
7
|
+
infer a clear name from the description.
|
|
8
|
+
- State the method signature: parameter names, types, defaults.
|
|
9
|
+
- State the return type and value.
|
|
10
|
+
- Describe the algorithm step by step.
|
|
11
|
+
- Translate vague terms into concrete Ruby operations. Examples:
|
|
12
|
+
"print in random places on the terminal" →
|
|
13
|
+
"Use ANSI escape codes (\e[row;colH) to move the cursor to
|
|
14
|
+
randomly chosen row/col positions within an 80×24 terminal grid,
|
|
15
|
+
then print the text at each position."
|
|
16
|
+
"returns a random direction" →
|
|
17
|
+
"Return one of the four cardinal direction symbols
|
|
18
|
+
[:north, :south, :east, :west] chosen with Array#sample."
|
|
19
|
+
- If the request mentions visual output, specify the exact mechanism
|
|
20
|
+
(ANSI escapes, $stdout.write, puts, etc.).
|
|
21
|
+
- If the request mentions randomness, specify using Ruby's rand / sample.
|
|
22
|
+
- Preserve every concrete detail the user gave — method names, parameter names,
|
|
23
|
+
Hash key names, return types, thresholds, formulas, counts, and strings.
|
|
24
|
+
Do NOT rename, abbreviate, or paraphrase any identifier.
|
|
25
|
+
- Do NOT generate Ruby code. Output ONLY the rewritten specification
|
|
26
|
+
as plain English paragraphs. No markdown fences, no code blocks.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Rewrite the following casual request into a precise Ruby method specification.
|
|
2
|
+
|
|
3
|
+
Class context:
|
|
4
|
+
- Class name: <%= class_name %>
|
|
5
|
+
- Instance variables: <%= ivars %>
|
|
6
|
+
- Public methods: <%= methods %>
|
|
7
|
+
- Scope: <%= scope_instruction %>
|
|
8
|
+
|
|
9
|
+
User request:
|
|
10
|
+
<%= raw_prompt %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SelfAgency
|
|
4
|
+
# Sandbox — shadows dangerous Kernel methods so generated code cannot
|
|
5
|
+
# call them. Included in an anonymous module that wraps every generated
|
|
6
|
+
# method, placing these shadows ahead of Kernel in Ruby's MRO.
|
|
7
|
+
module Sandbox
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def system(*) = raise(::SecurityError, "system() blocked by SelfAgency sandbox")
|
|
11
|
+
def exec(*) = raise(::SecurityError, "exec() blocked by SelfAgency sandbox")
|
|
12
|
+
def spawn(*) = raise(::SecurityError, "spawn() blocked by SelfAgency sandbox")
|
|
13
|
+
def fork(*) = raise(::SecurityError, "fork() blocked by SelfAgency sandbox")
|
|
14
|
+
def `(*) = raise(::SecurityError, "backtick execution blocked by SelfAgency sandbox")
|
|
15
|
+
def open(*) = raise(::SecurityError, "open() blocked by SelfAgency sandbox")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module SelfAgency
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Convert a String or Symbol to a CamelCase class name.
|
|
9
|
+
# :collector → "Collector"
|
|
10
|
+
# "weather_analyst" → "WeatherAnalyst"
|
|
11
|
+
# "WeatherAnalyst" → "WeatherAnalyst"
|
|
12
|
+
def self_agency_to_class_name(value)
|
|
13
|
+
str = value.to_s
|
|
14
|
+
return str if str.match?(/\A[A-Z]/)
|
|
15
|
+
|
|
16
|
+
str.split("_").map(&:capitalize).join
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Convert a CamelCase class name to snake_case.
|
|
20
|
+
# "WeatherAnalyst" → "weather_analyst"
|
|
21
|
+
def self_agency_to_snake_case(class_name)
|
|
22
|
+
class_name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Compute the require_relative path from the output file to the parent source.
|
|
26
|
+
def self_agency_relative_require(output_path, source_path)
|
|
27
|
+
output_dir = File.dirname(File.expand_path(output_path))
|
|
28
|
+
source_abs = File.expand_path(source_path)
|
|
29
|
+
|
|
30
|
+
Pathname.new(source_abs)
|
|
31
|
+
.relative_path_from(Pathname.new(output_dir))
|
|
32
|
+
.to_s
|
|
33
|
+
.sub(/\.rb\z/, "")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build the Ruby source string for a subclass file.
|
|
37
|
+
def self_agency_build_subclass_source(class_name, parent_class, require_path, sources, descriptions)
|
|
38
|
+
output = +"# frozen_string_literal: true\n\n"
|
|
39
|
+
output << "require_relative \"#{require_path}\"\n\n" if require_path
|
|
40
|
+
output << "class #{class_name} < #{parent_class}\n"
|
|
41
|
+
|
|
42
|
+
sources.each_with_index do |(name, code), index|
|
|
43
|
+
output << "\n" if index > 0
|
|
44
|
+
|
|
45
|
+
if (desc = descriptions[name])
|
|
46
|
+
desc.each_line { |line| output << " # #{line.chomp}\n" }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
code.each_line do |line|
|
|
50
|
+
if line.chomp.empty?
|
|
51
|
+
output << "\n"
|
|
52
|
+
else
|
|
53
|
+
output << " #{line}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
output << "\n" unless output.end_with?("\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
output << "end\n"
|
|
60
|
+
output
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SelfAgency
|
|
4
|
+
# Static-analysis patterns that must never appear in generated code
|
|
5
|
+
DANGEROUS_PATTERNS = /
|
|
6
|
+
\b(system|exec|spawn|fork|abort|exit)\b |
|
|
7
|
+
`[^`]*` |
|
|
8
|
+
%x\{ |
|
|
9
|
+
%x\[ |
|
|
10
|
+
%x\( |
|
|
11
|
+
\bFile\.\b |
|
|
12
|
+
\bIO\.\b |
|
|
13
|
+
\bKernel\.\b |
|
|
14
|
+
\bOpen3\.\b |
|
|
15
|
+
\bProcess\.\b |
|
|
16
|
+
\brequire\b |
|
|
17
|
+
\bload\b |
|
|
18
|
+
\b__send__\b |
|
|
19
|
+
\beval\b |
|
|
20
|
+
\bsend\b |
|
|
21
|
+
\bpublic_send\b |
|
|
22
|
+
\bmethod\s*\( |
|
|
23
|
+
\bconst_get\b |
|
|
24
|
+
\bclass_eval\b |
|
|
25
|
+
\bmodule_eval\b |
|
|
26
|
+
\binstance_eval\b |
|
|
27
|
+
\binstance_variable_set\b |
|
|
28
|
+
\binstance_variable_get\b |
|
|
29
|
+
\bdefine_method\b |
|
|
30
|
+
\bBinding\b |
|
|
31
|
+
\bBasicObject\b |
|
|
32
|
+
\bremove_method\b |
|
|
33
|
+
\bundef_method\b
|
|
34
|
+
/x
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Strip markdown fences, <think> blocks, and leading/trailing whitespace.
|
|
39
|
+
def self_agency_sanitize(raw)
|
|
40
|
+
text = raw.to_s.strip
|
|
41
|
+
text = text.sub(/\A```\w*\n?/, "").sub(/\n?```\s*\z/, "")
|
|
42
|
+
text = text.gsub(/<think>.*?<\/think>/m, "")
|
|
43
|
+
text.strip
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Validate the sanitized code. Raises on problems.
|
|
47
|
+
def self_agency_validate!(code)
|
|
48
|
+
raise ValidationError.new("code is empty", generated_code: code) if code.empty?
|
|
49
|
+
unless code.match?(/\bdef\s+\S+.*?\bend\b/m)
|
|
50
|
+
raise ValidationError.new("missing def...end structure", generated_code: code)
|
|
51
|
+
end
|
|
52
|
+
if (match = code.match(DANGEROUS_PATTERNS))
|
|
53
|
+
raise SecurityError.new(
|
|
54
|
+
"dangerous pattern detected: #{match[0].strip}",
|
|
55
|
+
matched_pattern: match[0].strip,
|
|
56
|
+
generated_code: code
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
RubyVM::InstructionSequence.compile(code)
|
|
61
|
+
rescue SyntaxError => e
|
|
62
|
+
raise ValidationError.new("syntax error: #{e.message}", generated_code: code)
|
|
63
|
+
end
|
|
64
|
+
end
|