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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +40 -0
  4. data/.irbrc +22 -0
  5. data/CHANGELOG.md +5 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +177 -0
  9. data/Rakefile +8 -0
  10. data/docs/api/configuration.md +85 -0
  11. data/docs/api/errors.md +166 -0
  12. data/docs/api/index.md +37 -0
  13. data/docs/api/self-agency-module.md +198 -0
  14. data/docs/architecture/overview.md +181 -0
  15. data/docs/architecture/security.md +101 -0
  16. data/docs/assets/images/self_agency.gif +0 -0
  17. data/docs/assets/images/self_agency.mp4 +0 -0
  18. data/docs/development/contributing.md +45 -0
  19. data/docs/development/setup.md +81 -0
  20. data/docs/development/testing.md +70 -0
  21. data/docs/examples/autonomous-robots.md +109 -0
  22. data/docs/examples/basic-examples.md +237 -0
  23. data/docs/examples/collaborative-robots.md +98 -0
  24. data/docs/examples/full-workflow.md +100 -0
  25. data/docs/examples/index.md +36 -0
  26. data/docs/getting-started/installation.md +71 -0
  27. data/docs/getting-started/quick-start.md +94 -0
  28. data/docs/guide/configuration.md +113 -0
  29. data/docs/guide/generating-methods.md +146 -0
  30. data/docs/guide/how-to-use.md +144 -0
  31. data/docs/guide/lifecycle-hooks.md +86 -0
  32. data/docs/guide/prompt-templates.md +189 -0
  33. data/docs/guide/saving-methods.md +84 -0
  34. data/docs/guide/scopes.md +74 -0
  35. data/docs/guide/source-inspection.md +96 -0
  36. data/docs/index.md +77 -0
  37. data/examples/01_basic_usage.rb +27 -0
  38. data/examples/02_multiple_methods.rb +43 -0
  39. data/examples/03_scopes.rb +40 -0
  40. data/examples/04_source_inspection.rb +46 -0
  41. data/examples/05_lifecycle_hook.rb +55 -0
  42. data/examples/06_configuration.rb +97 -0
  43. data/examples/07_error_handling.rb +103 -0
  44. data/examples/08_class_context.rb +64 -0
  45. data/examples/09_method_override.rb +52 -0
  46. data/examples/10_full_workflow.rb +118 -0
  47. data/examples/11_collaborative_robots/atlas.rb +31 -0
  48. data/examples/11_collaborative_robots/echo.rb +30 -0
  49. data/examples/11_collaborative_robots/main.rb +190 -0
  50. data/examples/11_collaborative_robots/nova.rb +71 -0
  51. data/examples/11_collaborative_robots/robot.rb +119 -0
  52. data/examples/12_autonomous_robots/analyst.rb +193 -0
  53. data/examples/12_autonomous_robots/collector.rb +78 -0
  54. data/examples/12_autonomous_robots/main.rb +166 -0
  55. data/examples/12_autonomous_robots/planner.rb +125 -0
  56. data/examples/12_autonomous_robots/robot.rb +284 -0
  57. data/examples/generated/from_range_class.rb +3 -0
  58. data/examples/generated/mean_instance.rb +4 -0
  59. data/examples/generated/median_instance.rb +15 -0
  60. data/examples/generated/report_singleton.rb +3 -0
  61. data/examples/generated/standard_deviation_instance.rb +8 -0
  62. data/examples/lib/message_bus.rb +57 -0
  63. data/examples/lib/setup.rb +8 -0
  64. data/lib/self_agency/configuration.rb +76 -0
  65. data/lib/self_agency/errors.rb +35 -0
  66. data/lib/self_agency/generator.rb +47 -0
  67. data/lib/self_agency/prompts/generate/system.txt.erb +15 -0
  68. data/lib/self_agency/prompts/generate/user.txt.erb +13 -0
  69. data/lib/self_agency/prompts/shape/system.txt.erb +26 -0
  70. data/lib/self_agency/prompts/shape/user.txt.erb +10 -0
  71. data/lib/self_agency/sandbox.rb +17 -0
  72. data/lib/self_agency/saver.rb +62 -0
  73. data/lib/self_agency/validator.rb +64 -0
  74. data/lib/self_agency/version.rb +5 -0
  75. data/lib/self_agency.rb +315 -0
  76. data/mkdocs.yml +156 -0
  77. data/sig/self_agency.rbs +4 -0
  78. 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,13 @@
1
+ <%= shaped_spec %>
2
+ <% if previous_error %>
3
+
4
+ Your previous attempt produced this code:
5
+
6
+ <%= previous_code %>
7
+
8
+ It failed with the following error:
9
+
10
+ <%= previous_error %>
11
+
12
+ Please fix the issue and generate corrected code.
13
+ <% end %>
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelfAgency
4
+ VERSION = "0.0.1"
5
+ end