rubyn-code 0.1.0 → 0.2.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +269 -467
  3. data/db/migrations/009_create_teams.sql +6 -6
  4. data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
  5. data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
  6. data/exe/rubyn-code +1 -1
  7. data/lib/rubyn_code/agent/RUBYN.md +17 -0
  8. data/lib/rubyn_code/agent/conversation.rb +68 -19
  9. data/lib/rubyn_code/agent/loop.rb +312 -54
  10. data/lib/rubyn_code/agent/loop_detector.rb +6 -6
  11. data/lib/rubyn_code/auth/RUBYN.md +19 -0
  12. data/lib/rubyn_code/auth/oauth.rb +40 -35
  13. data/lib/rubyn_code/auth/server.rb +16 -12
  14. data/lib/rubyn_code/auth/token_store.rb +22 -22
  15. data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
  16. data/lib/rubyn_code/autonomous/daemon.rb +115 -79
  17. data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
  18. data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
  19. data/lib/rubyn_code/background/RUBYN.md +13 -0
  20. data/lib/rubyn_code/background/notifier.rb +0 -2
  21. data/lib/rubyn_code/background/worker.rb +60 -15
  22. data/lib/rubyn_code/cli/RUBYN.md +30 -0
  23. data/lib/rubyn_code/cli/app.rb +85 -9
  24. data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
  25. data/lib/rubyn_code/cli/commands/base.rb +53 -0
  26. data/lib/rubyn_code/cli/commands/budget.rb +24 -0
  27. data/lib/rubyn_code/cli/commands/clear.rb +16 -0
  28. data/lib/rubyn_code/cli/commands/compact.rb +21 -0
  29. data/lib/rubyn_code/cli/commands/context.rb +44 -0
  30. data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
  31. data/lib/rubyn_code/cli/commands/cost.rb +23 -0
  32. data/lib/rubyn_code/cli/commands/diff.rb +30 -0
  33. data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
  34. data/lib/rubyn_code/cli/commands/help.rb +41 -0
  35. data/lib/rubyn_code/cli/commands/model.rb +37 -0
  36. data/lib/rubyn_code/cli/commands/plan.rb +22 -0
  37. data/lib/rubyn_code/cli/commands/quit.rb +17 -0
  38. data/lib/rubyn_code/cli/commands/registry.rb +64 -0
  39. data/lib/rubyn_code/cli/commands/resume.rb +51 -0
  40. data/lib/rubyn_code/cli/commands/review.rb +26 -0
  41. data/lib/rubyn_code/cli/commands/skill.rb +32 -0
  42. data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
  43. data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
  44. data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
  45. data/lib/rubyn_code/cli/commands/undo.rb +17 -0
  46. data/lib/rubyn_code/cli/commands/version.rb +16 -0
  47. data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
  48. data/lib/rubyn_code/cli/input_handler.rb +20 -23
  49. data/lib/rubyn_code/cli/renderer.rb +25 -27
  50. data/lib/rubyn_code/cli/repl.rb +161 -194
  51. data/lib/rubyn_code/cli/setup.rb +117 -0
  52. data/lib/rubyn_code/cli/spinner.rb +40 -40
  53. data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
  54. data/lib/rubyn_code/cli/version_check.rb +94 -0
  55. data/lib/rubyn_code/config/RUBYN.md +14 -0
  56. data/lib/rubyn_code/config/defaults.rb +28 -19
  57. data/lib/rubyn_code/config/project_config.rb +7 -9
  58. data/lib/rubyn_code/config/settings.rb +3 -3
  59. data/lib/rubyn_code/context/RUBYN.md +20 -0
  60. data/lib/rubyn_code/context/auto_compact.rb +7 -7
  61. data/lib/rubyn_code/context/compactor.rb +2 -2
  62. data/lib/rubyn_code/context/context_collapse.rb +45 -0
  63. data/lib/rubyn_code/context/manager.rb +20 -3
  64. data/lib/rubyn_code/context/manual_compact.rb +7 -7
  65. data/lib/rubyn_code/context/micro_compact.rb +12 -12
  66. data/lib/rubyn_code/db/RUBYN.md +40 -0
  67. data/lib/rubyn_code/db/connection.rb +13 -13
  68. data/lib/rubyn_code/db/migrator.rb +67 -27
  69. data/lib/rubyn_code/db/schema.rb +6 -6
  70. data/lib/rubyn_code/debug.rb +74 -0
  71. data/lib/rubyn_code/hooks/RUBYN.md +17 -0
  72. data/lib/rubyn_code/hooks/built_in.rb +9 -9
  73. data/lib/rubyn_code/hooks/registry.rb +5 -5
  74. data/lib/rubyn_code/hooks/runner.rb +1 -1
  75. data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
  76. data/lib/rubyn_code/learning/RUBYN.md +16 -0
  77. data/lib/rubyn_code/learning/extractor.rb +22 -22
  78. data/lib/rubyn_code/learning/injector.rb +17 -18
  79. data/lib/rubyn_code/learning/instinct.rb +18 -14
  80. data/lib/rubyn_code/llm/RUBYN.md +15 -0
  81. data/lib/rubyn_code/llm/client.rb +121 -55
  82. data/lib/rubyn_code/llm/message_builder.rb +19 -15
  83. data/lib/rubyn_code/llm/streaming.rb +80 -50
  84. data/lib/rubyn_code/mcp/RUBYN.md +21 -0
  85. data/lib/rubyn_code/mcp/client.rb +25 -24
  86. data/lib/rubyn_code/mcp/config.rb +7 -7
  87. data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
  88. data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
  89. data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
  90. data/lib/rubyn_code/memory/RUBYN.md +17 -0
  91. data/lib/rubyn_code/memory/models.rb +3 -3
  92. data/lib/rubyn_code/memory/search.rb +17 -17
  93. data/lib/rubyn_code/memory/session_persistence.rb +49 -34
  94. data/lib/rubyn_code/memory/store.rb +17 -17
  95. data/lib/rubyn_code/observability/RUBYN.md +19 -0
  96. data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
  97. data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
  98. data/lib/rubyn_code/observability/token_counter.rb +1 -1
  99. data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
  100. data/lib/rubyn_code/output/RUBYN.md +11 -0
  101. data/lib/rubyn_code/output/diff_renderer.rb +6 -6
  102. data/lib/rubyn_code/output/formatter.rb +4 -4
  103. data/lib/rubyn_code/permissions/RUBYN.md +17 -0
  104. data/lib/rubyn_code/permissions/prompter.rb +8 -8
  105. data/lib/rubyn_code/protocols/RUBYN.md +14 -0
  106. data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
  107. data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
  108. data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
  109. data/lib/rubyn_code/skills/RUBYN.md +19 -0
  110. data/lib/rubyn_code/skills/catalog.rb +7 -7
  111. data/lib/rubyn_code/skills/document.rb +15 -15
  112. data/lib/rubyn_code/skills/loader.rb +6 -8
  113. data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
  114. data/lib/rubyn_code/sub_agents/runner.rb +15 -15
  115. data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
  116. data/lib/rubyn_code/tasks/RUBYN.md +13 -0
  117. data/lib/rubyn_code/tasks/dag.rb +12 -16
  118. data/lib/rubyn_code/tasks/manager.rb +24 -24
  119. data/lib/rubyn_code/tasks/models.rb +4 -4
  120. data/lib/rubyn_code/teams/RUBYN.md +14 -0
  121. data/lib/rubyn_code/teams/mailbox.rb +38 -18
  122. data/lib/rubyn_code/teams/manager.rb +19 -19
  123. data/lib/rubyn_code/teams/teammate.rb +3 -4
  124. data/lib/rubyn_code/tools/RUBYN.md +38 -0
  125. data/lib/rubyn_code/tools/background_run.rb +9 -11
  126. data/lib/rubyn_code/tools/base.rb +54 -3
  127. data/lib/rubyn_code/tools/bash.rb +16 -34
  128. data/lib/rubyn_code/tools/bundle_add.rb +10 -12
  129. data/lib/rubyn_code/tools/bundle_install.rb +9 -11
  130. data/lib/rubyn_code/tools/compact.rb +10 -9
  131. data/lib/rubyn_code/tools/db_migrate.rb +17 -15
  132. data/lib/rubyn_code/tools/edit_file.rb +12 -12
  133. data/lib/rubyn_code/tools/executor.rb +9 -4
  134. data/lib/rubyn_code/tools/git_commit.rb +29 -34
  135. data/lib/rubyn_code/tools/git_diff.rb +17 -18
  136. data/lib/rubyn_code/tools/git_log.rb +17 -19
  137. data/lib/rubyn_code/tools/git_status.rb +18 -20
  138. data/lib/rubyn_code/tools/glob.rb +7 -9
  139. data/lib/rubyn_code/tools/grep.rb +11 -9
  140. data/lib/rubyn_code/tools/load_skill.rb +7 -7
  141. data/lib/rubyn_code/tools/memory_search.rb +13 -12
  142. data/lib/rubyn_code/tools/memory_write.rb +14 -12
  143. data/lib/rubyn_code/tools/rails_generate.rb +16 -16
  144. data/lib/rubyn_code/tools/read_file.rb +8 -7
  145. data/lib/rubyn_code/tools/read_inbox.rb +5 -5
  146. data/lib/rubyn_code/tools/registry.rb +2 -2
  147. data/lib/rubyn_code/tools/review_pr.rb +55 -55
  148. data/lib/rubyn_code/tools/run_specs.rb +20 -19
  149. data/lib/rubyn_code/tools/schema.rb +9 -11
  150. data/lib/rubyn_code/tools/send_message.rb +10 -10
  151. data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
  152. data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
  153. data/lib/rubyn_code/tools/task.rb +28 -28
  154. data/lib/rubyn_code/tools/web_fetch.rb +46 -31
  155. data/lib/rubyn_code/tools/web_search.rb +64 -66
  156. data/lib/rubyn_code/tools/write_file.rb +7 -6
  157. data/lib/rubyn_code/version.rb +1 -1
  158. data/lib/rubyn_code.rb +136 -105
  159. metadata +94 -21
@@ -14,14 +14,14 @@ CREATE INDEX IF NOT EXISTS idx_teammates_status ON teammates(status);
14
14
 
15
15
  CREATE TABLE IF NOT EXISTS mailbox_messages (
16
16
  id TEXT PRIMARY KEY,
17
- from_agent TEXT NOT NULL,
18
- to_agent TEXT NOT NULL,
19
- content TEXT NOT NULL,
20
- message_type TEXT NOT NULL DEFAULT 'text' CHECK(message_type IN ('text','task','result','error')),
17
+ sender TEXT NOT NULL,
18
+ recipient TEXT NOT NULL,
19
+ message_type TEXT NOT NULL DEFAULT 'message' CHECK(message_type IN ('message','task','result','error','broadcast','shutdown_request','shutdown_response','status_change')),
20
+ payload TEXT NOT NULL,
21
21
  read INTEGER NOT NULL DEFAULT 0,
22
22
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
23
23
  );
24
24
 
25
- CREATE INDEX IF NOT EXISTS idx_mailbox_to ON mailbox_messages(to_agent, read);
26
- CREATE INDEX IF NOT EXISTS idx_mailbox_from ON mailbox_messages(from_agent);
25
+ CREATE INDEX IF NOT EXISTS idx_mailbox_recipient_read ON mailbox_messages(recipient, read);
26
+ CREATE INDEX IF NOT EXISTS idx_mailbox_sender ON mailbox_messages(sender);
27
27
  CREATE INDEX IF NOT EXISTS idx_mailbox_created ON mailbox_messages(created_at);
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fix mailbox_messages column mismatch between 009_create_teams.sql and
4
+ # Teams::Mailbox. The original migration used from_agent/to_agent/content
5
+ # but the Mailbox class expects sender/recipient/payload.
6
+ #
7
+ # This is a Ruby migration (not SQL) because we need to detect which schema
8
+ # the user has before running the appropriate ALTER statements. Pure SQL
9
+ # can't branch on column existence without parse errors.
10
+ module Migration011FixMailboxMessagesColumns
11
+ module_function
12
+
13
+ # @param db [RubynCode::DB::Connection] the database connection
14
+ def up(db)
15
+ columns = db.query("SELECT name FROM pragma_table_info('mailbox_messages')").to_a
16
+ column_names = columns.map { |c| c['name'] }
17
+
18
+ if column_names.include?('from_agent')
19
+ # Old schema from 009 migration — rename columns
20
+ db.execute('ALTER TABLE mailbox_messages RENAME COLUMN from_agent TO sender')
21
+ db.execute('ALTER TABLE mailbox_messages RENAME COLUMN to_agent TO recipient')
22
+ db.execute('ALTER TABLE mailbox_messages RENAME COLUMN content TO payload')
23
+
24
+ # Remap 'text' message_type to 'message' to match Mailbox default
25
+ db.execute("UPDATE mailbox_messages SET message_type = 'message' WHERE message_type = 'text'")
26
+ end
27
+
28
+ # Ensure indexes match regardless of which schema path we took
29
+ db.execute('DROP INDEX IF EXISTS idx_mailbox_to')
30
+ db.execute('DROP INDEX IF EXISTS idx_mailbox_from')
31
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_recipient_read ON mailbox_messages(recipient, read)')
32
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_sender ON mailbox_messages(sender)')
33
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_created ON mailbox_messages(created_at)')
34
+ end
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Expands the mailbox_messages CHECK constraint to include protocol message types:
4
+ # shutdown_request, shutdown_response, status_change
5
+ #
6
+ # SQLite does not support ALTER CONSTRAINT, so we rebuild the table.
7
+ # The Migrator already wraps .up in a transaction — no manual BEGIN/COMMIT here.
8
+ module Migration012ExpandMailboxMessageTypes
9
+ module_function
10
+
11
+ def up(db)
12
+ db.execute(<<~SQL)
13
+ CREATE TABLE mailbox_messages_new (
14
+ id TEXT PRIMARY KEY,
15
+ sender TEXT NOT NULL,
16
+ recipient TEXT NOT NULL,
17
+ message_type TEXT NOT NULL DEFAULT 'message'
18
+ CHECK(message_type IN ('message','task','result','error','broadcast','shutdown_request','shutdown_response','status_change')),
19
+ payload TEXT NOT NULL,
20
+ read INTEGER NOT NULL DEFAULT 0,
21
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
22
+ )
23
+ SQL
24
+
25
+ db.execute(<<~SQL)
26
+ INSERT INTO mailbox_messages_new (id, sender, recipient, message_type, payload, read, created_at)
27
+ SELECT id, sender, recipient, message_type, payload, read, created_at
28
+ FROM mailbox_messages
29
+ SQL
30
+
31
+ db.execute('DROP TABLE mailbox_messages')
32
+ db.execute('ALTER TABLE mailbox_messages_new RENAME TO mailbox_messages')
33
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_recipient_read ON mailbox_messages(recipient, read)')
34
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_sender ON mailbox_messages(sender)')
35
+ db.execute('CREATE INDEX IF NOT EXISTS idx_mailbox_created ON mailbox_messages(created_at)')
36
+ end
37
+ end
data/exe/rubyn-code CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "rubyn_code"
4
+ require 'rubyn_code'
5
5
 
6
6
  RubynCode::CLI::App.start(ARGV)
@@ -0,0 +1,17 @@
1
+ # Layer 1: Agent
2
+
3
+ The core agentic loop. This is the heartbeat of the whole system.
4
+
5
+ ## Classes
6
+
7
+ - **`Loop`** — The main agent loop. Sends conversation to Claude, receives a response.
8
+ If the response contains `tool_use` blocks, dispatches them via `Tools::Executor`,
9
+ appends results, and loops. Stops when Claude returns plain text, budget is exhausted,
10
+ or `MAX_ITERATIONS` is reached. Collaborates with `LoopDetector` to break stalls.
11
+
12
+ - **`Conversation`** — In-memory conversation state. Holds the messages array (user turns,
13
+ assistant turns, tool results). Supports undo, clear, and context compaction.
14
+
15
+ - **`LoopDetector`** — Detects when the agent is stuck calling the same tool with the same
16
+ arguments. Uses a sliding window (default: 5) with a threshold (default: 3 identical calls).
17
+ Raises `StallDetectedError` when triggered.
@@ -14,7 +14,7 @@ module RubynCode
14
14
  # @param content [String]
15
15
  # @return [Hash] the appended message
16
16
  def add_user_message(content)
17
- message = { role: "user", content: content }
17
+ message = { role: 'user', content: content }
18
18
  @messages << message
19
19
  message
20
20
  end
@@ -26,7 +26,7 @@ module RubynCode
26
26
  # @return [Hash] the appended message
27
27
  def add_assistant_message(content, tool_calls: [])
28
28
  blocks = normalize_content(content, tool_calls)
29
- message = { role: "assistant", content: blocks }
29
+ message = { role: 'assistant', content: blocks }
30
30
  @messages << message
31
31
  message
32
32
  end
@@ -38,9 +38,9 @@ module RubynCode
38
38
  # @param output [String]
39
39
  # @param is_error [Boolean]
40
40
  # @return [Hash] the appended message
41
- def add_tool_result(tool_use_id, tool_name, output, is_error: false)
41
+ def add_tool_result(tool_use_id, _tool_name, output, is_error: false)
42
42
  result_block = {
43
- type: "tool_result",
43
+ type: 'tool_result',
44
44
  tool_use_id: tool_use_id,
45
45
  content: output.to_s
46
46
  }
@@ -50,10 +50,10 @@ module RubynCode
50
50
  # is an array of tool_result blocks. When the previous message is
51
51
  # already a user/tool_result message we append to it so that multiple
52
52
  # tool results for the same assistant turn are batched together.
53
- if @messages.last && @messages.last[:role] == "user" && tool_result_message?(@messages.last)
53
+ if @messages.last && @messages.last[:role] == 'user' && tool_result_message?(@messages.last)
54
54
  @messages.last[:content] << result_block
55
55
  else
56
- @messages << { role: "user", content: [result_block] }
56
+ @messages << { role: 'user', content: [result_block] }
57
57
  end
58
58
 
59
59
  result_block
@@ -63,7 +63,7 @@ module RubynCode
63
63
  #
64
64
  # @return [String, nil]
65
65
  def last_assistant_text
66
- assistant_msg = @messages.reverse_each.find { |m| m[:role] == "assistant" }
66
+ assistant_msg = @messages.reverse_each.find { |m| m[:role] == 'assistant' }
67
67
  return nil unless assistant_msg
68
68
 
69
69
  extract_text(assistant_msg[:content])
@@ -86,12 +86,14 @@ module RubynCode
86
86
  #
87
87
  # @return [Array<Hash>]
88
88
  def to_api_format
89
- @messages.map do |msg|
89
+ formatted = @messages.map do |msg|
90
90
  {
91
91
  role: msg[:role],
92
92
  content: format_content(msg[:content])
93
93
  }
94
94
  end
95
+
96
+ repair_orphaned_tool_uses(formatted)
95
97
  end
96
98
 
97
99
  # Remove the last user + assistant exchange. Useful for undo.
@@ -108,15 +110,62 @@ module RubynCode
108
110
  removed = 0
109
111
  while @messages.any? && removed < 2
110
112
  last = @messages.last
111
- break if removed == 1 && last[:role] != "assistant" && last[:role] != "user"
113
+ break if removed == 1 && last[:role] != 'assistant' && last[:role] != 'user'
112
114
 
113
115
  @messages.pop
114
116
  removed += 1
115
117
  end
116
118
  end
117
119
 
120
+ # Replace messages with a new array (used after compaction).
121
+ def replace!(new_messages)
122
+ @messages.replace(new_messages)
123
+ end
124
+
118
125
  private
119
126
 
127
+ # Ensure every tool_use block has a matching tool_result.
128
+ # If a tool_use is orphaned (e.g. from Ctrl-C interruption),
129
+ # inject a synthetic tool_result so the API doesn't reject the request.
130
+ def repair_orphaned_tool_uses(formatted)
131
+ # Collect all tool_use IDs from assistant messages
132
+ tool_use_ids = Set.new
133
+ formatted.each do |msg|
134
+ next unless msg[:role] == 'assistant' && msg[:content].is_a?(Array)
135
+
136
+ msg[:content].each do |block|
137
+ if block.is_a?(Hash) && (block[:type] == 'tool_use' || block['type'] == 'tool_use')
138
+ tool_use_ids << (block[:id] || block['id'])
139
+ end
140
+ end
141
+ end
142
+
143
+ # Collect all tool_result IDs from user messages
144
+ tool_result_ids = Set.new
145
+ formatted.each do |msg|
146
+ next unless msg[:role] == 'user' && msg[:content].is_a?(Array)
147
+
148
+ msg[:content].each do |block|
149
+ if block.is_a?(Hash) && (block[:type] == 'tool_result' || block['type'] == 'tool_result')
150
+ tool_result_ids << (block[:tool_use_id] || block['tool_use_id'])
151
+ end
152
+ end
153
+ end
154
+
155
+ # Find orphans
156
+ orphaned = tool_use_ids - tool_result_ids
157
+ return formatted if orphaned.empty?
158
+
159
+ # Inject synthetic tool_results for orphans
160
+ orphan_results = orphaned.map do |id|
161
+ { type: 'tool_result', tool_use_id: id, content: '[interrupted]', is_error: true }
162
+ end
163
+
164
+ # Append as a user message after the last assistant message
165
+ formatted << { role: 'user', content: orphan_results }
166
+ formatted
167
+ end
168
+
120
169
  # Normalize content and tool_calls into a single array of content blocks.
121
170
  def normalize_content(content, tool_calls)
122
171
  blocks = []
@@ -125,7 +174,7 @@ module RubynCode
125
174
  when Array
126
175
  content.each { |b| blocks << block_to_hash(b) }
127
176
  when String
128
- blocks << { type: "text", text: content } unless content.empty?
177
+ blocks << { type: 'text', text: content } unless content.empty?
129
178
  when Hash
130
179
  blocks << content
131
180
  else
@@ -145,7 +194,7 @@ module RubynCode
145
194
  when String then content
146
195
  when Array
147
196
  content.map { |block| block_to_hash(block) }
148
- else ""
197
+ else ''
149
198
  end
150
199
  end
151
200
 
@@ -154,12 +203,12 @@ module RubynCode
154
203
 
155
204
  if block.respond_to?(:type)
156
205
  case block.type.to_s
157
- when "text"
158
- { type: "text", text: block.text }
159
- when "tool_use"
160
- { type: "tool_use", id: block.id, name: block.name, input: block.input }
161
- when "tool_result"
162
- h = { type: "tool_result", tool_use_id: block.tool_use_id, content: block.content.to_s }
206
+ when 'text'
207
+ { type: 'text', text: block.text }
208
+ when 'tool_use'
209
+ { type: 'tool_use', id: block.id, name: block.name, input: block.input }
210
+ when 'tool_result'
211
+ h = { type: 'tool_result', tool_use_id: block.tool_use_id, content: block.content.to_s }
163
212
  h[:is_error] = true if block.respond_to?(:is_error) && block.is_error
164
213
  h
165
214
  else
@@ -176,7 +225,7 @@ module RubynCode
176
225
  when String
177
226
  content
178
227
  when Array
179
- text_blocks = content.select { |b| b.is_a?(Hash) && b[:type] == "text" }
228
+ text_blocks = content.select { |b| b.is_a?(Hash) && b[:type] == 'text' }
180
229
  texts = text_blocks.map { |b| b[:text] }
181
230
  texts.empty? ? nil : texts.join("\n")
182
231
  end
@@ -186,7 +235,7 @@ module RubynCode
186
235
  def tool_result_message?(msg)
187
236
  return false unless msg[:content].is_a?(Array)
188
237
 
189
- msg[:content].all? { |b| b.is_a?(Hash) && b[:type] == "tool_result" }
238
+ msg[:content].all? { |b| b.is_a?(Hash) && b[:type] == 'tool_result' }
190
239
  end
191
240
  end
192
241
  end