agent_c 2.9

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 (65) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +10 -0
  3. data/.ruby-version +1 -0
  4. data/CLAUDE.md +21 -0
  5. data/README.md +360 -0
  6. data/Rakefile +16 -0
  7. data/TODO.md +105 -0
  8. data/agent_c.gemspec +38 -0
  9. data/docs/batch.md +503 -0
  10. data/docs/chat-methods.md +156 -0
  11. data/docs/cost-reporting.md +86 -0
  12. data/docs/pipeline-tips-and-tricks.md +453 -0
  13. data/docs/session-configuration.md +274 -0
  14. data/docs/testing.md +747 -0
  15. data/docs/tools.md +103 -0
  16. data/docs/versioned-store.md +840 -0
  17. data/lib/agent_c/agent/chat.rb +211 -0
  18. data/lib/agent_c/agent/chat_response.rb +38 -0
  19. data/lib/agent_c/agent/chats/anthropic_bedrock.rb +48 -0
  20. data/lib/agent_c/batch.rb +102 -0
  21. data/lib/agent_c/configs/repo.rb +90 -0
  22. data/lib/agent_c/context.rb +56 -0
  23. data/lib/agent_c/costs/data.rb +39 -0
  24. data/lib/agent_c/costs/report.rb +219 -0
  25. data/lib/agent_c/db/store.rb +162 -0
  26. data/lib/agent_c/errors.rb +19 -0
  27. data/lib/agent_c/pipeline.rb +152 -0
  28. data/lib/agent_c/pipelines/agent.rb +219 -0
  29. data/lib/agent_c/processor.rb +98 -0
  30. data/lib/agent_c/prompts.yml +53 -0
  31. data/lib/agent_c/schema.rb +71 -0
  32. data/lib/agent_c/session.rb +206 -0
  33. data/lib/agent_c/store.rb +72 -0
  34. data/lib/agent_c/test_helpers.rb +173 -0
  35. data/lib/agent_c/tools/dir_glob.rb +46 -0
  36. data/lib/agent_c/tools/edit_file.rb +114 -0
  37. data/lib/agent_c/tools/file_metadata.rb +43 -0
  38. data/lib/agent_c/tools/git_status.rb +30 -0
  39. data/lib/agent_c/tools/grep.rb +119 -0
  40. data/lib/agent_c/tools/paths.rb +36 -0
  41. data/lib/agent_c/tools/read_file.rb +94 -0
  42. data/lib/agent_c/tools/run_rails_test.rb +87 -0
  43. data/lib/agent_c/tools.rb +61 -0
  44. data/lib/agent_c/utils/git.rb +87 -0
  45. data/lib/agent_c/utils/shell.rb +58 -0
  46. data/lib/agent_c/version.rb +5 -0
  47. data/lib/agent_c.rb +32 -0
  48. data/lib/versioned_store/base.rb +314 -0
  49. data/lib/versioned_store/config.rb +26 -0
  50. data/lib/versioned_store/stores/schema.rb +127 -0
  51. data/lib/versioned_store/version.rb +5 -0
  52. data/lib/versioned_store.rb +5 -0
  53. data/template/Gemfile +9 -0
  54. data/template/Gemfile.lock +152 -0
  55. data/template/README.md +61 -0
  56. data/template/Rakefile +50 -0
  57. data/template/bin/rake +27 -0
  58. data/template/lib/autoload.rb +10 -0
  59. data/template/lib/config.rb +59 -0
  60. data/template/lib/pipeline.rb +19 -0
  61. data/template/lib/prompts.yml +57 -0
  62. data/template/lib/store.rb +17 -0
  63. data/template/test/pipeline_test.rb +221 -0
  64. data/template/test/test_helper.rb +18 -0
  65. metadata +194 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ module VersionedStore
5
+ module Stores
6
+ class Schema
7
+ Record = Data.define(:name, :table, :blocks)
8
+ Migration = Data.define(:version, :block)
9
+
10
+ class RecordContext
11
+ attr_reader :schema_instance, :table_name, :record_name
12
+
13
+ def initialize(schema_instance, record_name)
14
+ @schema_instance = schema_instance
15
+ @record_name = record_name
16
+ @table_name = nil
17
+ end
18
+
19
+ def schema(table_name = nil, &block)
20
+ # If no table name provided, infer from record name
21
+ if table_name.nil?
22
+ name_str = record_name.to_s
23
+ table_name = (name_str.end_with?('s') ? name_str : "#{name_str}s").to_sym
24
+ end
25
+
26
+ # Store the table name for later use
27
+ @table_name = table_name
28
+
29
+ # Collect all schema blocks for a table
30
+ schema_instance.schema_blocks[table_name] ||= []
31
+ schema_instance.schema_blocks[table_name] << block if block
32
+ end
33
+
34
+ def method_missing(...)
35
+ end
36
+
37
+ def respond_to_missing?(...)
38
+ true
39
+ end
40
+ end
41
+
42
+ attr_reader :migrations, :records, :migrated_tables, :schema_blocks, :post_init_hooks
43
+ def initialize
44
+ @migrations = []
45
+ @records = {}
46
+ @migration_counter = 1
47
+ @migrated_tables = Set.new
48
+ @schema_blocks = {}
49
+ @post_init_hooks = []
50
+ end
51
+
52
+ def dup
53
+ new_schema = Schema.new
54
+ new_schema.instance_variable_set(:@migrations, @migrations.dup)
55
+ new_schema.instance_variable_set(:@records, @records.dup)
56
+ new_schema.instance_variable_set(:@migration_counter, @migration_counter)
57
+ new_schema.instance_variable_set(:@migrated_tables, @migrated_tables.dup)
58
+ new_schema.instance_variable_set(:@schema_blocks, @schema_blocks.dup)
59
+ new_schema.instance_variable_set(:@post_init_hooks, @post_init_hooks.dup)
60
+ new_schema.instance_variable_set(:@dir, @dir)
61
+ new_schema
62
+ end
63
+
64
+ def dir(path = nil)
65
+ @dir = path if path
66
+ @dir
67
+ end
68
+
69
+ def migrate(version = @migration_counter += 1, &block)
70
+ migrations << Migration.new(version: version, block: block)
71
+ end
72
+
73
+ def prepend_migration(version, &block)
74
+ migrations.unshift(Migration.new(version: version, block: block))
75
+ end
76
+
77
+ def record(name, table: nil, &block)
78
+ # If block is given, execute it in RecordContext to extract schema calls
79
+ extracted_table = table
80
+ if block
81
+ context = RecordContext.new(self, name)
82
+ context.instance_exec(&block)
83
+ extracted_table ||= context.table_name
84
+ end
85
+
86
+ prev = records[name]
87
+ blocks = prev ? prev.blocks.dup : []
88
+ blocks << block if block
89
+ # Prefer the first non-nil table name, otherwise default to name + "s"
90
+ table_name = prev&.table || extracted_table
91
+ if table_name.nil?
92
+ name_str = name.to_s
93
+ table_name = (name_str.end_with?('s') ? name_str : "#{name_str}s").to_sym
94
+ end
95
+
96
+ records[name] = Record.new(
97
+ name: name,
98
+ table: table_name,
99
+ blocks: blocks
100
+ )
101
+ end
102
+
103
+ # Add a migration for each table with collected schema blocks (called after all record blocks)
104
+ def add_table_migrations!
105
+ schema_blocks.each do |table_name, blocks|
106
+ next if migrated_tables.include?(table_name) || table_name.nil?
107
+ blocks_to_eval = blocks.dup
108
+ migration_block = Proc.new do
109
+ create_table(table_name) do |t|
110
+ blocks_to_eval.each { |blk| blk.call(t) }
111
+ end
112
+ end
113
+ version = "table_#{table_name}"
114
+ prepend_migration(version, &migration_block)
115
+ migrated_tables.add(table_name)
116
+ end
117
+ end
118
+
119
+ def self.call(&block)
120
+ schema = new
121
+ schema.instance_exec(&block) if block
122
+ schema.add_table_migrations!
123
+ schema
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VersionedStore
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module VersionedStore
4
+ class Error < StandardError; end
5
+ end
data/template/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "agent_c", path: "../../agent_c"
6
+
7
+ gem "rake"
8
+ gem "minitest"
9
+ gem "debug"
@@ -0,0 +1,152 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ agent_c (2.71828)
5
+ activerecord
6
+ async
7
+ json-schema
8
+ ruby_llm
9
+ sqlite3
10
+ zeitwerk
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ activemodel (8.1.2)
16
+ activesupport (= 8.1.2)
17
+ activerecord (8.1.2)
18
+ activemodel (= 8.1.2)
19
+ activesupport (= 8.1.2)
20
+ timeout (>= 0.4.0)
21
+ activesupport (8.1.2)
22
+ base64
23
+ bigdecimal
24
+ concurrent-ruby (~> 1.0, >= 1.3.1)
25
+ connection_pool (>= 2.2.5)
26
+ drb
27
+ i18n (>= 1.6, < 2)
28
+ json
29
+ logger (>= 1.4.2)
30
+ minitest (>= 5.1)
31
+ securerandom (>= 0.3)
32
+ tzinfo (~> 2.0, >= 2.0.5)
33
+ uri (>= 0.13.1)
34
+ addressable (2.8.8)
35
+ public_suffix (>= 2.0.2, < 8.0)
36
+ async (2.36.0)
37
+ console (~> 1.29)
38
+ fiber-annotation
39
+ io-event (~> 1.11)
40
+ metrics (~> 0.12)
41
+ traces (~> 0.18)
42
+ base64 (0.3.0)
43
+ bigdecimal (4.0.1)
44
+ concurrent-ruby (1.3.6)
45
+ connection_pool (3.0.2)
46
+ console (1.34.2)
47
+ fiber-annotation
48
+ fiber-local (~> 1.1)
49
+ json
50
+ date (3.5.1)
51
+ debug (1.11.1)
52
+ irb (~> 1.10)
53
+ reline (>= 0.3.8)
54
+ drb (2.2.3)
55
+ erb (6.0.1)
56
+ event_stream_parser (1.0.0)
57
+ faraday (2.14.0)
58
+ faraday-net_http (>= 2.0, < 3.5)
59
+ json
60
+ logger
61
+ faraday-multipart (1.2.0)
62
+ multipart-post (~> 2.0)
63
+ faraday-net_http (3.4.2)
64
+ net-http (~> 0.5)
65
+ faraday-retry (2.4.0)
66
+ faraday (~> 2.0)
67
+ fiber-annotation (0.2.0)
68
+ fiber-local (1.1.0)
69
+ fiber-storage
70
+ fiber-storage (1.0.1)
71
+ i18n (1.14.8)
72
+ concurrent-ruby (~> 1.0)
73
+ io-console (0.8.2)
74
+ io-event (1.14.2)
75
+ irb (1.16.0)
76
+ pp (>= 0.6.0)
77
+ rdoc (>= 4.0.0)
78
+ reline (>= 0.4.2)
79
+ json (2.18.0)
80
+ json-schema (6.1.0)
81
+ addressable (~> 2.8)
82
+ bigdecimal (>= 3.1, < 5)
83
+ logger (1.7.0)
84
+ marcel (1.1.0)
85
+ metrics (0.15.0)
86
+ minitest (6.0.1)
87
+ prism (~> 1.5)
88
+ multipart-post (2.4.1)
89
+ net-http (0.9.1)
90
+ uri (>= 0.11.1)
91
+ pp (0.6.3)
92
+ prettyprint
93
+ prettyprint (0.2.0)
94
+ prism (1.8.0)
95
+ psych (5.3.1)
96
+ date
97
+ stringio
98
+ public_suffix (7.0.2)
99
+ rake (13.3.1)
100
+ rdoc (7.1.0)
101
+ erb
102
+ psych (>= 4.0.0)
103
+ tsort
104
+ reline (0.6.3)
105
+ io-console (~> 0.5)
106
+ ruby_llm (1.11.0)
107
+ base64
108
+ event_stream_parser (~> 1)
109
+ faraday (>= 1.10.0)
110
+ faraday-multipart (>= 1)
111
+ faraday-net_http (>= 1)
112
+ faraday-retry (>= 1)
113
+ marcel (~> 1.0)
114
+ ruby_llm-schema (~> 0.2.1)
115
+ zeitwerk (~> 2)
116
+ ruby_llm-schema (0.2.5)
117
+ securerandom (0.4.1)
118
+ sqlite3 (2.9.0-aarch64-linux-gnu)
119
+ sqlite3 (2.9.0-aarch64-linux-musl)
120
+ sqlite3 (2.9.0-arm-linux-gnu)
121
+ sqlite3 (2.9.0-arm-linux-musl)
122
+ sqlite3 (2.9.0-arm64-darwin)
123
+ sqlite3 (2.9.0-x86_64-darwin)
124
+ sqlite3 (2.9.0-x86_64-linux-gnu)
125
+ sqlite3 (2.9.0-x86_64-linux-musl)
126
+ stringio (3.2.0)
127
+ timeout (0.6.0)
128
+ traces (0.18.2)
129
+ tsort (0.2.0)
130
+ tzinfo (2.0.6)
131
+ concurrent-ruby (~> 1.0)
132
+ uri (1.1.1)
133
+ zeitwerk (2.7.4)
134
+
135
+ PLATFORMS
136
+ aarch64-linux-gnu
137
+ aarch64-linux-musl
138
+ arm-linux-gnu
139
+ arm-linux-musl
140
+ arm64-darwin
141
+ x86_64-darwin
142
+ x86_64-linux-gnu
143
+ x86_64-linux-musl
144
+
145
+ DEPENDENCIES
146
+ agent_c!
147
+ debug
148
+ minitest
149
+ rake
150
+
151
+ BUNDLED WITH
152
+ 2.6.5
@@ -0,0 +1,61 @@
1
+ # Overview
2
+
3
+ This directory contains a small script demonstrating how to run a batch of pipelines.
4
+
5
+ For each `summary` record created, Claude will choose a random file, summarize it, and write the summary to disk. Then our pipeline commits the changes at the end.
6
+
7
+ It creates two records and runs them across two worktrees.
8
+
9
+ The main entrypoint is the Rakefile.
10
+
11
+
12
+ ### Running the Batch
13
+
14
+ You'll need to set the relevant environment variables specified in the Config. (The environment variables necessary assume you're using Claude on Bedrock.)
15
+
16
+ Then run:
17
+
18
+ ```sh
19
+ bin/rake run
20
+ # => Summary report:
21
+ # => Succeeded: 2
22
+ # => Pending: 0
23
+ # => Failed: 0
24
+ # => Run cost: $0.31
25
+ # => Project total cost: $1.67
26
+ # => ---
27
+ # => task: 1 - wrote summary to /tmp/example-worktrees/summary-examples-0/VERSIONED_STORE_BASE_SUMMARY.md
28
+ # => task: 2 - wrote summary to /tmp/example-worktrees/summary-examples-1/SUMMARY-es.md
29
+ ```
30
+
31
+ ### Watching progress
32
+
33
+ You can tail the logs with:
34
+
35
+ ```sh
36
+ # See EVERYTHING
37
+ tail -f log/run.log
38
+
39
+ # See just progress
40
+ tail -f log/run.log | grep INFO
41
+ ```
42
+
43
+ ### Poking around the data
44
+
45
+ ```sh
46
+ bin/rake console
47
+ ```
48
+
49
+ ### Resetting state
50
+
51
+ Delete the state stored in the `./tmp` directory to start all over:
52
+
53
+ ```sh
54
+ rm -rf ./tmp
55
+ ```
56
+
57
+ ### Run the tests:
58
+
59
+ ```sh
60
+ bin/rake test
61
+ ```
data/template/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "rake/testtask"
5
+
6
+ require_relative "./lib/autoload"
7
+
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << "test"
10
+ t.libs << "lib"
11
+ t.test_files = FileList["test/**/*_test.rb"]
12
+ t.verbose = true
13
+ t.warning = false
14
+ end
15
+
16
+ desc "Run the pipeline"
17
+ task :run do
18
+ batch = AgentC::Batch.new(**Config::BATCH)
19
+
20
+ ["english", "spanish"].map do |language|
21
+ record = batch.store.summary.find_or_create_by!(language:)
22
+
23
+ batch.add_task(record)
24
+ end
25
+
26
+ batch.call
27
+
28
+ puts "Summary report:"
29
+ puts batch.report
30
+ puts "---\n"
31
+
32
+ batch.store.task.all.each do |task|
33
+ next unless task.done?
34
+
35
+ full_path = File.join(
36
+ task.workspace.dir,
37
+ task.record.summary_path
38
+ )
39
+ puts "task: #{task.id} - wrote summary to #{full_path}"
40
+ end
41
+ end
42
+
43
+ task :console do
44
+ batch = AgentC::Batch.new(**Config::BATCH)
45
+
46
+ # poke around the data here
47
+ binding.irb
48
+ end
49
+
50
+ task default: :test
data/template/bin/rake ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rake' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("rake", "rake")
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "versioned_store"
4
+ require "agent_c"
5
+ require "i18n"
6
+ require "zeitwerk"
7
+
8
+ loader = Zeitwerk::Loader.new
9
+ loader.push_dir(File.expand_path(__dir__))
10
+ loader.setup
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "tmpdir"
5
+
6
+ module Config
7
+ LOG_PATH = "./log/run.log"
8
+ FileUtils.mkdir_p(File.dirname(LOG_PATH))
9
+
10
+ LOGGER = Logger.new(LOG_PATH)
11
+
12
+ PROJECT = "TemplateProject.v1"
13
+
14
+ BATCH = {
15
+ record_type: :summary,
16
+ pipeline: Pipeline,
17
+
18
+ store: {
19
+ class: Store,
20
+ config: {
21
+ logger: LOGGER,
22
+ dir: File.join(
23
+ File.expand_path("../tmp", __dir__),
24
+ PROJECT,
25
+ )
26
+ },
27
+ },
28
+
29
+ repo: {
30
+ dir: File.expand_path("../../", __dir__),
31
+ initial_revision: "main",
32
+ working_subdir: "", # use the root-level of the repo
33
+ worktrees_root_dir: "/tmp/example-worktrees",
34
+ worktree_branch_prefix: "summary-examples",
35
+ worktree_envs: [
36
+ {
37
+ SOME_ENV: "1",
38
+ },
39
+ {
40
+ SOME_ENV: "2",
41
+ },
42
+ ],
43
+ },
44
+
45
+ session: {
46
+ agent_db_path: File.expand_path("../../tmp/claude.sqlite", __dir__),
47
+ logger: LOGGER,
48
+ i18n_path: File.expand_path("prompts.yml", __dir__),
49
+ project: PROJECT,
50
+ ruby_llm: {
51
+ bedrock_api_key: ENV.fetch("AWS_ACCESS_KEY_ID"),
52
+ bedrock_secret_key: ENV.fetch("AWS_SECRET_ACCESS_KEY"),
53
+ bedrock_session_token: ENV.fetch("AWS_SESSION_TOKEN"),
54
+ bedrock_region: ENV.fetch("AWS_REGION", "us-west-2"),
55
+ default_model: ENV.fetch("LLM_MODEL", "us.anthropic.claude-sonnet-4-5-20250929-v1:0")
56
+ }
57
+ },
58
+ }
59
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pipeline < AgentC::Pipeline
4
+ agent_step(:pick_a_random_file)
5
+ agent_step(:summarize_the_file)
6
+ agent_step(:write_summary_to_disk)
7
+
8
+ step(:finalize) do
9
+ if repo.uncommitted_changes?
10
+ repo.commit_all(
11
+ <<~TXT
12
+ claude: added file: #{record.summary_path}
13
+ TXT
14
+ )
15
+ else
16
+ task.fail!("didn't create a file")
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,57 @@
1
+ en:
2
+ # This prompt will be used by all steps, it will be cached
3
+ # to save $$$.
4
+ global_summary_cache: &global_summary_cache |
5
+ This project is a ruby gem. You will be summarizing a file using
6
+ the language specified.
7
+ pick_a_random_file:
8
+ tools: [read_file, grep, dir_glob]
9
+ cached_prompts:
10
+ - *global_summary_cache
11
+ prompt: |
12
+ # YOUR JOB
13
+
14
+ Find a random ruby file within the repository. Bonus points for picking
15
+ a file with many lines.
16
+ response_schema:
17
+ input_path:
18
+ description: |
19
+ The path to the file you have chosen
20
+
21
+ summarize_the_file:
22
+ tools: [read_file]
23
+ cached_prompts:
24
+ - *global_summary_cache
25
+ prompt: |
26
+ You will be given a file path. Your job is to summarize the file for a
27
+ developer to read and understand.
28
+
29
+ You must write your summary using the language provided.
30
+
31
+ language: %{language}
32
+ file path: %{input_path}
33
+ response_schema:
34
+ summary_body:
35
+ description: |
36
+ Your summary of the file
37
+
38
+ write_summary_to_disk:
39
+ tools: [edit_file]
40
+ cached_prompts:
41
+ - *global_summary_cache
42
+ prompt: |
43
+ You will be given a file path and some summary text. Write the text to the
44
+ a well-named file at the top-level of the repository. You will return the
45
+ path that you wrote.
46
+
47
+ IMPORTANT: you **must** invoke the edit_file tool to process this request.
48
+
49
+ summary:
50
+ ---BEGIN-SUMMARY---
51
+ %{summary_body}
52
+ ---END-SUMMARY---
53
+
54
+ response_schema:
55
+ summary_path:
56
+ description: |
57
+ The path to the file you wrote.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Store < VersionedStore::Base
4
+ include AgentC::Store
5
+
6
+ record(:summary) do
7
+ schema do |t|
8
+ # we'll input this data
9
+ t.string(:language)
10
+
11
+ # claude will generate this data
12
+ t.string(:input_path)
13
+ t.string(:summary_body)
14
+ t.string(:summary_path)
15
+ end
16
+ end
17
+ end