rubyn-code 0.2.2 → 0.4.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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +151 -5
  3. data/db/migrations/013_add_failed_status_to_tasks.rb +51 -0
  4. data/lib/rubyn_code/agent/background_job_handler.rb +71 -0
  5. data/lib/rubyn_code/agent/conversation.rb +84 -56
  6. data/lib/rubyn_code/agent/dynamic_tool_schema.rb +152 -0
  7. data/lib/rubyn_code/agent/feedback_handler.rb +49 -0
  8. data/lib/rubyn_code/agent/llm_caller.rb +157 -0
  9. data/lib/rubyn_code/agent/loop.rb +182 -683
  10. data/lib/rubyn_code/agent/loop_detector.rb +50 -11
  11. data/lib/rubyn_code/agent/prompts.rb +109 -0
  12. data/lib/rubyn_code/agent/response_modes.rb +111 -0
  13. data/lib/rubyn_code/agent/response_parser.rb +111 -0
  14. data/lib/rubyn_code/agent/system_prompt_builder.rb +211 -0
  15. data/lib/rubyn_code/agent/tool_processor.rb +178 -0
  16. data/lib/rubyn_code/agent/usage_tracker.rb +59 -0
  17. data/lib/rubyn_code/auth/key_encryption.rb +118 -0
  18. data/lib/rubyn_code/auth/oauth.rb +80 -64
  19. data/lib/rubyn_code/auth/server.rb +21 -24
  20. data/lib/rubyn_code/auth/token_store.rb +80 -52
  21. data/lib/rubyn_code/autonomous/daemon.rb +146 -32
  22. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -24
  23. data/lib/rubyn_code/autonomous/task_claimer.rb +46 -44
  24. data/lib/rubyn_code/background/worker.rb +64 -76
  25. data/lib/rubyn_code/cli/app.rb +159 -114
  26. data/lib/rubyn_code/cli/commands/doctor.rb +73 -0
  27. data/lib/rubyn_code/cli/commands/mcp.rb +77 -0
  28. data/lib/rubyn_code/cli/commands/model.rb +105 -18
  29. data/lib/rubyn_code/cli/commands/new_session.rb +45 -0
  30. data/lib/rubyn_code/cli/commands/provider.rb +123 -0
  31. data/lib/rubyn_code/cli/commands/skill.rb +52 -3
  32. data/lib/rubyn_code/cli/daemon_runner.rb +64 -11
  33. data/lib/rubyn_code/cli/first_run.rb +159 -0
  34. data/lib/rubyn_code/cli/renderer.rb +109 -60
  35. data/lib/rubyn_code/cli/repl.rb +48 -374
  36. data/lib/rubyn_code/cli/repl_commands.rb +177 -0
  37. data/lib/rubyn_code/cli/repl_lifecycle.rb +76 -0
  38. data/lib/rubyn_code/cli/repl_setup.rb +181 -0
  39. data/lib/rubyn_code/cli/setup.rb +6 -2
  40. data/lib/rubyn_code/cli/stream_formatter.rb +56 -49
  41. data/lib/rubyn_code/cli/version_check.rb +28 -11
  42. data/lib/rubyn_code/config/defaults.rb +11 -0
  43. data/lib/rubyn_code/config/project_profile.rb +185 -0
  44. data/lib/rubyn_code/config/schema.json +49 -0
  45. data/lib/rubyn_code/config/settings.rb +103 -1
  46. data/lib/rubyn_code/config/validator.rb +63 -0
  47. data/lib/rubyn_code/context/auto_compact.rb +1 -1
  48. data/lib/rubyn_code/context/context_budget.rb +182 -0
  49. data/lib/rubyn_code/context/context_collapse.rb +34 -4
  50. data/lib/rubyn_code/context/decision_compactor.rb +99 -0
  51. data/lib/rubyn_code/context/manager.rb +44 -8
  52. data/lib/rubyn_code/context/manual_compact.rb +1 -1
  53. data/lib/rubyn_code/context/micro_compact.rb +29 -19
  54. data/lib/rubyn_code/context/schema_filter.rb +64 -0
  55. data/lib/rubyn_code/db/connection.rb +31 -26
  56. data/lib/rubyn_code/db/migrator.rb +44 -28
  57. data/lib/rubyn_code/hooks/built_in.rb +14 -10
  58. data/lib/rubyn_code/hooks/registry.rb +4 -0
  59. data/lib/rubyn_code/ide/adapters/tool_output.rb +330 -0
  60. data/lib/rubyn_code/ide/client.rb +110 -0
  61. data/lib/rubyn_code/ide/handlers/accept_edit_handler.rb +35 -0
  62. data/lib/rubyn_code/ide/handlers/approve_tool_use_handler.rb +34 -0
  63. data/lib/rubyn_code/ide/handlers/cancel_handler.rb +41 -0
  64. data/lib/rubyn_code/ide/handlers/config_get_handler.rb +63 -0
  65. data/lib/rubyn_code/ide/handlers/config_set_handler.rb +86 -0
  66. data/lib/rubyn_code/ide/handlers/initialize_handler.rb +79 -0
  67. data/lib/rubyn_code/ide/handlers/models_list_handler.rb +39 -0
  68. data/lib/rubyn_code/ide/handlers/prompt_handler.rb +215 -0
  69. data/lib/rubyn_code/ide/handlers/review_handler.rb +110 -0
  70. data/lib/rubyn_code/ide/handlers/session_fork_handler.rb +49 -0
  71. data/lib/rubyn_code/ide/handlers/session_list_handler.rb +41 -0
  72. data/lib/rubyn_code/ide/handlers/session_reset_handler.rb +31 -0
  73. data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +42 -0
  74. data/lib/rubyn_code/ide/handlers/shutdown_handler.rb +37 -0
  75. data/lib/rubyn_code/ide/handlers.rb +76 -0
  76. data/lib/rubyn_code/ide/protocol.rb +111 -0
  77. data/lib/rubyn_code/ide/server.rb +186 -0
  78. data/lib/rubyn_code/index/codebase_index.rb +311 -0
  79. data/lib/rubyn_code/learning/extractor.rb +65 -82
  80. data/lib/rubyn_code/learning/injector.rb +22 -23
  81. data/lib/rubyn_code/learning/instinct.rb +71 -42
  82. data/lib/rubyn_code/learning/shortcut.rb +95 -0
  83. data/lib/rubyn_code/llm/adapters/anthropic.rb +274 -0
  84. data/lib/rubyn_code/llm/adapters/anthropic_compatible.rb +60 -0
  85. data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +215 -0
  86. data/lib/rubyn_code/llm/adapters/base.rb +35 -0
  87. data/lib/rubyn_code/llm/adapters/json_parsing.rb +21 -0
  88. data/lib/rubyn_code/llm/adapters/openai.rb +246 -0
  89. data/lib/rubyn_code/llm/adapters/openai_compatible.rb +50 -0
  90. data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +90 -0
  91. data/lib/rubyn_code/llm/adapters/openai_streaming.rb +141 -0
  92. data/lib/rubyn_code/llm/adapters/prompt_caching.rb +60 -0
  93. data/lib/rubyn_code/llm/client.rb +75 -247
  94. data/lib/rubyn_code/llm/model_router.rb +237 -0
  95. data/lib/rubyn_code/llm/streaming.rb +4 -227
  96. data/lib/rubyn_code/mcp/client.rb +1 -1
  97. data/lib/rubyn_code/mcp/config.rb +10 -12
  98. data/lib/rubyn_code/mcp/sse_transport.rb +15 -13
  99. data/lib/rubyn_code/mcp/stdio_transport.rb +16 -18
  100. data/lib/rubyn_code/mcp/tool_bridge.rb +31 -62
  101. data/lib/rubyn_code/memory/search.rb +1 -0
  102. data/lib/rubyn_code/memory/session_persistence.rb +59 -58
  103. data/lib/rubyn_code/memory/store.rb +42 -55
  104. data/lib/rubyn_code/observability/budget_enforcer.rb +46 -32
  105. data/lib/rubyn_code/observability/cost_calculator.rb +32 -8
  106. data/lib/rubyn_code/observability/skill_analytics.rb +116 -0
  107. data/lib/rubyn_code/observability/token_analytics.rb +130 -0
  108. data/lib/rubyn_code/observability/usage_reporter.rb +79 -61
  109. data/lib/rubyn_code/output/diff_renderer.rb +102 -77
  110. data/lib/rubyn_code/output/formatter.rb +11 -11
  111. data/lib/rubyn_code/permissions/policy.rb +11 -13
  112. data/lib/rubyn_code/permissions/prompter.rb +8 -9
  113. data/lib/rubyn_code/protocols/plan_approval.rb +25 -20
  114. data/lib/rubyn_code/self_test.rb +315 -0
  115. data/lib/rubyn_code/skills/catalog.rb +66 -0
  116. data/lib/rubyn_code/skills/document.rb +33 -29
  117. data/lib/rubyn_code/skills/loader.rb +43 -0
  118. data/lib/rubyn_code/skills/ttl_manager.rb +100 -0
  119. data/lib/rubyn_code/sub_agents/runner.rb +20 -25
  120. data/lib/rubyn_code/tasks/dag.rb +25 -24
  121. data/lib/rubyn_code/tasks/models.rb +1 -0
  122. data/lib/rubyn_code/tools/ask_user.rb +44 -0
  123. data/lib/rubyn_code/tools/background_run.rb +2 -1
  124. data/lib/rubyn_code/tools/base.rb +39 -32
  125. data/lib/rubyn_code/tools/bash.rb +7 -1
  126. data/lib/rubyn_code/tools/edit_file.rb +130 -17
  127. data/lib/rubyn_code/tools/executor.rb +130 -25
  128. data/lib/rubyn_code/tools/file_cache.rb +95 -0
  129. data/lib/rubyn_code/tools/git_commit.rb +12 -10
  130. data/lib/rubyn_code/tools/git_log.rb +12 -10
  131. data/lib/rubyn_code/tools/glob.rb +29 -7
  132. data/lib/rubyn_code/tools/grep.rb +8 -1
  133. data/lib/rubyn_code/tools/ide_diagnostics.rb +51 -0
  134. data/lib/rubyn_code/tools/ide_symbols.rb +53 -0
  135. data/lib/rubyn_code/tools/load_skill.rb +13 -6
  136. data/lib/rubyn_code/tools/memory_search.rb +14 -13
  137. data/lib/rubyn_code/tools/memory_write.rb +2 -1
  138. data/lib/rubyn_code/tools/output_compressor.rb +190 -0
  139. data/lib/rubyn_code/tools/read_file.rb +17 -6
  140. data/lib/rubyn_code/tools/registry.rb +11 -0
  141. data/lib/rubyn_code/tools/review_pr.rb +127 -80
  142. data/lib/rubyn_code/tools/run_specs.rb +26 -15
  143. data/lib/rubyn_code/tools/schema.rb +4 -10
  144. data/lib/rubyn_code/tools/spawn_agent.rb +113 -82
  145. data/lib/rubyn_code/tools/spawn_teammate.rb +107 -64
  146. data/lib/rubyn_code/tools/spec_output_parser.rb +118 -0
  147. data/lib/rubyn_code/tools/task.rb +17 -17
  148. data/lib/rubyn_code/tools/web_fetch.rb +62 -47
  149. data/lib/rubyn_code/tools/web_search.rb +66 -48
  150. data/lib/rubyn_code/tools/write_file.rb +76 -1
  151. data/lib/rubyn_code/version.rb +1 -1
  152. data/lib/rubyn_code.rb +62 -1
  153. data/skills/rubyn_self_test.md +133 -0
  154. metadata +83 -1
@@ -68,7 +68,7 @@ module RubynCode
68
68
  ]
69
69
 
70
70
  options = {}
71
- options[:model] = 'claude-sonnet-4-20250514' if llm_client.respond_to?(:chat)
71
+ options[:model] = 'claude-sonnet-4-6' if llm_client.respond_to?(:chat)
72
72
 
73
73
  response = llm_client.chat(messages: summary_messages, **options)
74
74
 
@@ -22,22 +22,27 @@ module RubynCode
22
22
 
23
23
  tool_name_index = build_tool_name_index(messages)
24
24
  candidates = tool_result_refs[0..-(keep_recent + 1)]
25
- compacted = 0
25
+ compact_candidates(candidates, tool_name_index, preserve_tools)
26
+ end
26
27
 
28
+ def self.compact_candidates(candidates, tool_name_index, preserve_tools)
29
+ compacted = 0
27
30
  candidates.each do |ref|
28
- block = ref[:block]
29
- content = extract_content(block)
30
- next if content.nil? || content.length < MIN_CONTENT_LENGTH
31
+ compacted += 1 if compact_single_ref(ref, tool_name_index, preserve_tools)
32
+ end
33
+ compacted
34
+ end
31
35
 
32
- tool_name = resolve_tool_name(block, tool_name_index)
33
- next if preserve_tools.include?(tool_name)
36
+ def self.compact_single_ref(ref, tool_name_index, preserve_tools) # rubocop:disable Naming/PredicateMethod -- returns boolean but is an action method
37
+ block = ref[:block]
38
+ content = extract_content(block)
39
+ return false if content.nil? || content.length < MIN_CONTENT_LENGTH
34
40
 
35
- placeholder = format(PLACEHOLDER_TEMPLATE, tool_name: tool_name || 'tool')
36
- replace_content!(block, placeholder)
37
- compacted += 1
38
- end
41
+ tool_name = resolve_tool_name(block, tool_name_index)
42
+ return false if preserve_tools.include?(tool_name)
39
43
 
40
- compacted
44
+ replace_content!(block, format(PLACEHOLDER_TEMPLATE, tool_name: tool_name || 'tool'))
45
+ true
41
46
  end
42
47
 
43
48
  # Collects all tool_result content blocks across user messages, preserving
@@ -70,19 +75,23 @@ module RubynCode
70
75
  messages.each do |msg|
71
76
  next unless msg[:role] == 'assistant' && msg[:content].is_a?(Array)
72
77
 
73
- msg[:content].each do |block|
74
- case block
75
- when Hash
76
- index[block[:id] || block['id']] = block[:name] || block['name'] if block_type(block) == 'tool_use'
77
- when LLM::ToolUseBlock
78
- index[block.id] = block.name
79
- end
80
- end
78
+ msg[:content].each { |block| index_tool_use(index, block) }
81
79
  end
82
80
 
83
81
  index
84
82
  end
85
83
 
84
+ def self.index_tool_use(index, block)
85
+ case block
86
+ when Hash
87
+ return unless block_type(block) == 'tool_use'
88
+
89
+ index[block[:id] || block['id']] = block[:name] || block['name']
90
+ when LLM::ToolUseBlock
91
+ index[block.id] = block.name
92
+ end
93
+ end
94
+
86
95
  def self.tool_result_block?(block)
87
96
  case block
88
97
  when Hash
@@ -128,6 +137,7 @@ module RubynCode
128
137
  end
129
138
 
130
139
  private_class_method :collect_tool_results, :build_tool_name_index,
140
+ :index_tool_use, :compact_candidates, :compact_single_ref,
131
141
  :tool_result_block?, :block_type, :extract_content,
132
142
  :resolve_tool_name, :replace_content!
133
143
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubynCode
4
+ module Context
5
+ # Extracts only the relevant table definitions from db/schema.rb
6
+ # based on which models are currently in context. Loading the full
7
+ # schema for a large Rails app can be 5-10K tokens; filtering to
8
+ # relevant tables typically reduces this to 200-500 tokens.
9
+ module SchemaFilter
10
+ TABLE_PATTERN = /create_table\s+"([^"]+)"/
11
+ END_PATTERN = /\A\s+end\s*\z/
12
+
13
+ class << self
14
+ # Returns schema definitions for only the specified table names.
15
+ #
16
+ # @param schema_path [String] path to db/schema.rb
17
+ # @param table_names [Array<String>] table names to include
18
+ # @return [String] filtered schema content
19
+ def filter(schema_path, table_names:)
20
+ return '' if table_names.empty?
21
+ return '' unless File.exist?(schema_path)
22
+
23
+ lines = File.readlines(schema_path)
24
+ extract_tables(lines, table_names.to_set(&:to_s))
25
+ end
26
+
27
+ # Derives table names from model class names using Rails conventions.
28
+ #
29
+ # @param model_names [Array<String>] e.g., ["User", "OrderItem"]
30
+ # @return [Array<String>] e.g., ["users", "order_items"]
31
+ def tableize(model_names)
32
+ model_names.map { |name| "#{name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}s" }
33
+ end
34
+
35
+ # Convenience: filter schema by model names instead of table names.
36
+ def filter_for_models(schema_path, model_names:)
37
+ tables = tableize(model_names)
38
+ filter(schema_path, table_names: tables)
39
+ end
40
+
41
+ private
42
+
43
+ def extract_tables(lines, table_set)
44
+ result = []
45
+ capturing = false
46
+
47
+ lines.each do |line|
48
+ match = TABLE_PATTERN.match(line)
49
+ capturing = true if match && table_set.include?(match[1])
50
+
51
+ result << line if capturing
52
+
53
+ if capturing && END_PATTERN.match?(line)
54
+ capturing = false
55
+ result << "\n"
56
+ end
57
+ end
58
+
59
+ result.join
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -106,34 +106,11 @@ module RubynCode
106
106
  #
107
107
  # @yield the block to execute
108
108
  # @return [Object] the block's return value
109
- def transaction
109
+ def transaction(&block)
110
110
  synchronize do
111
- if @transaction_depth.zero?
112
- begin_top_level_transaction
113
- else
114
- begin_savepoint
115
- end
116
-
111
+ @transaction_depth.zero? ? begin_top_level_transaction : begin_savepoint
117
112
  @transaction_depth += 1
118
- begin
119
- result = yield
120
- if @transaction_depth == 1
121
- @db.execute('COMMIT')
122
- else
123
- @db.execute("RELEASE SAVEPOINT sp_#{@transaction_depth}")
124
- end
125
- result
126
- rescue StandardError => e
127
- if @transaction_depth == 1
128
- @db.execute('ROLLBACK')
129
- else
130
- @db.execute("ROLLBACK TO SAVEPOINT sp_#{@transaction_depth}")
131
- @db.execute("RELEASE SAVEPOINT sp_#{@transaction_depth}")
132
- end
133
- raise e
134
- ensure
135
- @transaction_depth -= 1
136
- end
113
+ execute_transaction_body(&block)
137
114
  end
138
115
  end
139
116
 
@@ -171,6 +148,34 @@ module RubynCode
171
148
  def begin_savepoint
172
149
  @db.execute("SAVEPOINT sp_#{@transaction_depth + 1}")
173
150
  end
151
+
152
+ def execute_transaction_body
153
+ result = yield
154
+ commit_or_release
155
+ result
156
+ rescue StandardError => e
157
+ rollback_or_release
158
+ raise e
159
+ ensure
160
+ @transaction_depth -= 1
161
+ end
162
+
163
+ def commit_or_release
164
+ if @transaction_depth == 1
165
+ @db.execute('COMMIT')
166
+ else
167
+ @db.execute("RELEASE SAVEPOINT sp_#{@transaction_depth}")
168
+ end
169
+ end
170
+
171
+ def rollback_or_release
172
+ if @transaction_depth == 1
173
+ @db.execute('ROLLBACK')
174
+ else
175
+ @db.execute("ROLLBACK TO SAVEPOINT sp_#{@transaction_depth}")
176
+ @db.execute("RELEASE SAVEPOINT sp_#{@transaction_depth}")
177
+ end
178
+ end
174
179
  end
175
180
  end
176
181
  end
@@ -38,7 +38,7 @@ module RubynCode
38
38
  # @return [Array<Array(Integer, String)>] pairs of [version, file_path]
39
39
  def pending_migrations
40
40
  applied = applied_versions
41
- available_migrations.reject { |version, _| applied.include?(version) }
41
+ available_migrations.reject { |version, _| applied.include?(version) } # rubocop:disable Style/HashExcept
42
42
  end
43
43
 
44
44
  # Returns the set of already-applied migration versions.
@@ -67,16 +67,16 @@ module RubynCode
67
67
  def available_migrations
68
68
  all = Dir.glob(File.join(MIGRATIONS_DIR, '*'))
69
69
  .select { |path| path.end_with?('.sql', '.rb') }
70
- .map { |path| parse_migration_file(path) }
71
- .compact
70
+ .filter_map { |path| parse_migration_file(path) }
72
71
 
73
- # Deduplicate: if both .rb and .sql exist for the same version, prefer .rb
72
+ deduplicate_migrations(all)
73
+ end
74
+
75
+ def deduplicate_migrations(all)
74
76
  by_version = {}
75
77
  all.each do |version, path|
76
- existing = by_version[version]
77
- by_version[version] = [version, path] if existing.nil? || path.end_with?('.rb')
78
+ by_version[version] = [version, path] if !by_version[version] || path.end_with?('.rb')
78
79
  end
79
-
80
80
  by_version.values.sort_by(&:first)
81
81
  end
82
82
 
@@ -139,33 +139,49 @@ module RubynCode
139
139
  in_block = false
140
140
 
141
141
  sql.each_line do |line|
142
- stripped = line.strip
142
+ in_block, current = process_sql_line(line, statements, current, in_block)
143
+ end
143
144
 
144
- # Track BEGIN/END blocks (e.g., triggers)
145
- if stripped.match?(/\bBEGIN\b/i) && !stripped.match?(/\ABEGIN\s+(IMMEDIATE|DEFERRED|EXCLUSIVE)/i)
146
- in_block = true
147
- end
148
- current << line
149
-
150
- if in_block
151
- if stripped.match?(/\bEND\b\s*;?\s*$/i)
152
- in_block = false
153
- statements << current.strip.chomp(';')
154
- current = +''
155
- end
156
- elsif stripped.end_with?(';')
157
- stmt = current.strip.chomp(';').strip
158
- statements << stmt unless stmt.empty? || (stmt.match?(/\A\s*--/) && !stmt.include?("\n"))
159
- current = +''
160
- end
145
+ finalize_statements(statements, current)
146
+ end
147
+
148
+ def process_sql_line(line, statements, current, in_block)
149
+ stripped = line.strip
150
+ in_block = true if begin_block?(stripped)
151
+ current << line
152
+
153
+ if in_block && stripped.match?(/\bEND\b\s*;?\s*$/i)
154
+ statements << current.strip.chomp(';')
155
+ [false, +'']
156
+ elsif !in_block && stripped.end_with?(';')
157
+ append_statement(statements, current)
158
+ [false, +'']
159
+ else
160
+ [in_block, current]
161
161
  end
162
+ end
163
+
164
+ def begin_block?(stripped)
165
+ stripped.match?(/\bBEGIN\b/i) &&
166
+ !stripped.match?(/\ABEGIN\s+(IMMEDIATE|DEFERRED|EXCLUSIVE)/i)
167
+ end
168
+
169
+ def append_statement(statements, current)
170
+ stmt = current.strip.chomp(';').strip
171
+ return if stmt.empty? || (stmt.match?(/\A\s*--/) && !stmt.include?("\n"))
162
172
 
163
- # Handle any remaining content
173
+ statements << stmt
174
+ end
175
+
176
+ def finalize_statements(statements, current)
164
177
  remainder = current.strip.chomp(';').strip
165
178
  statements << remainder unless remainder.empty?
166
179
 
167
- # Filter out comment-only statements
168
- statements.reject { |s| s.lines.all? { |l| l.strip.empty? || l.strip.start_with?('--') } }
180
+ statements.reject { |s| comment_only?(s) }
181
+ end
182
+
183
+ def comment_only?(stmt)
184
+ stmt.lines.all? { |l| l.strip.empty? || l.strip.start_with?('--') }
169
185
  end
170
186
 
171
187
  # Extracts the version number and name from a migration filename.
@@ -25,20 +25,24 @@ module RubynCode
25
25
  usage = response[:usage] || response['usage']
26
26
  return unless usage
27
27
 
28
- model = response[:model] || response['model'] || 'unknown'
29
- input_tokens = usage[:input_tokens] || usage['input_tokens'] || 0
30
- output_tokens = usage[:output_tokens] || usage['output_tokens'] || 0
31
- cache_read = usage[:cache_read_input_tokens] || usage['cache_read_input_tokens'] || 0
32
- cache_write = usage[:cache_creation_input_tokens] || usage['cache_creation_input_tokens'] || 0
28
+ record_usage(response, usage)
29
+ end
30
+
31
+ private
33
32
 
33
+ def record_usage(response, usage)
34
34
  @budget_enforcer.record!(
35
- model: model,
36
- input_tokens: input_tokens,
37
- output_tokens: output_tokens,
38
- cache_read_tokens: cache_read,
39
- cache_write_tokens: cache_write
35
+ model: fetch_value(response, :model, 'unknown'),
36
+ input_tokens: fetch_value(usage, :input_tokens, 0),
37
+ output_tokens: fetch_value(usage, :output_tokens, 0),
38
+ cache_read_tokens: fetch_value(usage, :cache_read_input_tokens, 0),
39
+ cache_write_tokens: fetch_value(usage, :cache_creation_input_tokens, 0)
40
40
  )
41
41
  end
42
+
43
+ def fetch_value(hash, sym_key, default)
44
+ hash[sym_key] || hash[sym_key.to_s] || default
45
+ end
42
46
  end
43
47
 
44
48
  # Logs tool calls and their results via the formatter.
@@ -17,6 +17,10 @@ module RubynCode
17
17
  on_stall
18
18
  on_error
19
19
  on_session_end
20
+ session_start
21
+ user_prompt_submit
22
+ permission_request
23
+ stop
20
24
  ].freeze
21
25
 
22
26
  Hook = Data.define(:callable, :priority)