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.
- checksums.yaml +4 -4
- data/README.md +269 -467
- data/db/migrations/009_create_teams.sql +6 -6
- data/db/migrations/011_fix_mailbox_messages_columns.rb +35 -0
- data/db/migrations/012_expand_mailbox_message_types.rb +37 -0
- data/exe/rubyn-code +1 -1
- data/lib/rubyn_code/agent/RUBYN.md +17 -0
- data/lib/rubyn_code/agent/conversation.rb +68 -19
- data/lib/rubyn_code/agent/loop.rb +312 -54
- data/lib/rubyn_code/agent/loop_detector.rb +6 -6
- data/lib/rubyn_code/auth/RUBYN.md +19 -0
- data/lib/rubyn_code/auth/oauth.rb +40 -35
- data/lib/rubyn_code/auth/server.rb +16 -12
- data/lib/rubyn_code/auth/token_store.rb +22 -22
- data/lib/rubyn_code/autonomous/RUBYN.md +14 -0
- data/lib/rubyn_code/autonomous/daemon.rb +115 -79
- data/lib/rubyn_code/autonomous/idle_poller.rb +4 -8
- data/lib/rubyn_code/autonomous/task_claimer.rb +11 -11
- data/lib/rubyn_code/background/RUBYN.md +13 -0
- data/lib/rubyn_code/background/notifier.rb +0 -2
- data/lib/rubyn_code/background/worker.rb +60 -15
- data/lib/rubyn_code/cli/RUBYN.md +30 -0
- data/lib/rubyn_code/cli/app.rb +85 -9
- data/lib/rubyn_code/cli/commands/RUBYN.md +133 -0
- data/lib/rubyn_code/cli/commands/base.rb +53 -0
- data/lib/rubyn_code/cli/commands/budget.rb +24 -0
- data/lib/rubyn_code/cli/commands/clear.rb +16 -0
- data/lib/rubyn_code/cli/commands/compact.rb +21 -0
- data/lib/rubyn_code/cli/commands/context.rb +44 -0
- data/lib/rubyn_code/cli/commands/context_info.rb +56 -0
- data/lib/rubyn_code/cli/commands/cost.rb +23 -0
- data/lib/rubyn_code/cli/commands/diff.rb +30 -0
- data/lib/rubyn_code/cli/commands/doctor.rb +112 -0
- data/lib/rubyn_code/cli/commands/help.rb +41 -0
- data/lib/rubyn_code/cli/commands/model.rb +37 -0
- data/lib/rubyn_code/cli/commands/plan.rb +22 -0
- data/lib/rubyn_code/cli/commands/quit.rb +17 -0
- data/lib/rubyn_code/cli/commands/registry.rb +64 -0
- data/lib/rubyn_code/cli/commands/resume.rb +51 -0
- data/lib/rubyn_code/cli/commands/review.rb +26 -0
- data/lib/rubyn_code/cli/commands/skill.rb +32 -0
- data/lib/rubyn_code/cli/commands/spawn.rb +24 -0
- data/lib/rubyn_code/cli/commands/tasks.rb +32 -0
- data/lib/rubyn_code/cli/commands/tokens.rb +76 -0
- data/lib/rubyn_code/cli/commands/undo.rb +17 -0
- data/lib/rubyn_code/cli/commands/version.rb +16 -0
- data/lib/rubyn_code/cli/daemon_runner.rb +129 -0
- data/lib/rubyn_code/cli/input_handler.rb +20 -23
- data/lib/rubyn_code/cli/renderer.rb +25 -27
- data/lib/rubyn_code/cli/repl.rb +161 -194
- data/lib/rubyn_code/cli/setup.rb +117 -0
- data/lib/rubyn_code/cli/spinner.rb +40 -40
- data/lib/rubyn_code/cli/stream_formatter.rb +29 -28
- data/lib/rubyn_code/cli/version_check.rb +94 -0
- data/lib/rubyn_code/config/RUBYN.md +14 -0
- data/lib/rubyn_code/config/defaults.rb +28 -19
- data/lib/rubyn_code/config/project_config.rb +7 -9
- data/lib/rubyn_code/config/settings.rb +3 -3
- data/lib/rubyn_code/context/RUBYN.md +20 -0
- data/lib/rubyn_code/context/auto_compact.rb +7 -7
- data/lib/rubyn_code/context/compactor.rb +2 -2
- data/lib/rubyn_code/context/context_collapse.rb +45 -0
- data/lib/rubyn_code/context/manager.rb +20 -3
- data/lib/rubyn_code/context/manual_compact.rb +7 -7
- data/lib/rubyn_code/context/micro_compact.rb +12 -12
- data/lib/rubyn_code/db/RUBYN.md +40 -0
- data/lib/rubyn_code/db/connection.rb +13 -13
- data/lib/rubyn_code/db/migrator.rb +67 -27
- data/lib/rubyn_code/db/schema.rb +6 -6
- data/lib/rubyn_code/debug.rb +74 -0
- data/lib/rubyn_code/hooks/RUBYN.md +17 -0
- data/lib/rubyn_code/hooks/built_in.rb +9 -9
- data/lib/rubyn_code/hooks/registry.rb +5 -5
- data/lib/rubyn_code/hooks/runner.rb +1 -1
- data/lib/rubyn_code/hooks/user_hooks.rb +16 -16
- data/lib/rubyn_code/learning/RUBYN.md +16 -0
- data/lib/rubyn_code/learning/extractor.rb +22 -22
- data/lib/rubyn_code/learning/injector.rb +17 -18
- data/lib/rubyn_code/learning/instinct.rb +18 -14
- data/lib/rubyn_code/llm/RUBYN.md +15 -0
- data/lib/rubyn_code/llm/client.rb +121 -55
- data/lib/rubyn_code/llm/message_builder.rb +19 -15
- data/lib/rubyn_code/llm/streaming.rb +80 -50
- data/lib/rubyn_code/mcp/RUBYN.md +21 -0
- data/lib/rubyn_code/mcp/client.rb +25 -24
- data/lib/rubyn_code/mcp/config.rb +7 -7
- data/lib/rubyn_code/mcp/sse_transport.rb +27 -26
- data/lib/rubyn_code/mcp/stdio_transport.rb +22 -19
- data/lib/rubyn_code/mcp/tool_bridge.rb +32 -32
- data/lib/rubyn_code/memory/RUBYN.md +17 -0
- data/lib/rubyn_code/memory/models.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +17 -17
- data/lib/rubyn_code/memory/session_persistence.rb +49 -34
- data/lib/rubyn_code/memory/store.rb +17 -17
- data/lib/rubyn_code/observability/RUBYN.md +19 -0
- data/lib/rubyn_code/observability/budget_enforcer.rb +16 -15
- data/lib/rubyn_code/observability/cost_calculator.rb +3 -3
- data/lib/rubyn_code/observability/token_counter.rb +1 -1
- data/lib/rubyn_code/observability/usage_reporter.rb +35 -35
- data/lib/rubyn_code/output/RUBYN.md +11 -0
- data/lib/rubyn_code/output/diff_renderer.rb +6 -6
- data/lib/rubyn_code/output/formatter.rb +4 -4
- data/lib/rubyn_code/permissions/RUBYN.md +17 -0
- data/lib/rubyn_code/permissions/prompter.rb +8 -8
- data/lib/rubyn_code/protocols/RUBYN.md +14 -0
- data/lib/rubyn_code/protocols/interrupt_handler.rb +1 -1
- data/lib/rubyn_code/protocols/plan_approval.rb +9 -9
- data/lib/rubyn_code/protocols/shutdown_handshake.rb +9 -11
- data/lib/rubyn_code/skills/RUBYN.md +19 -0
- data/lib/rubyn_code/skills/catalog.rb +7 -7
- data/lib/rubyn_code/skills/document.rb +15 -15
- data/lib/rubyn_code/skills/loader.rb +6 -8
- data/lib/rubyn_code/sub_agents/RUBYN.md +12 -0
- data/lib/rubyn_code/sub_agents/runner.rb +15 -15
- data/lib/rubyn_code/sub_agents/summarizer.rb +1 -1
- data/lib/rubyn_code/tasks/RUBYN.md +13 -0
- data/lib/rubyn_code/tasks/dag.rb +12 -16
- data/lib/rubyn_code/tasks/manager.rb +24 -24
- data/lib/rubyn_code/tasks/models.rb +4 -4
- data/lib/rubyn_code/teams/RUBYN.md +14 -0
- data/lib/rubyn_code/teams/mailbox.rb +38 -18
- data/lib/rubyn_code/teams/manager.rb +19 -19
- data/lib/rubyn_code/teams/teammate.rb +3 -4
- data/lib/rubyn_code/tools/RUBYN.md +38 -0
- data/lib/rubyn_code/tools/background_run.rb +9 -11
- data/lib/rubyn_code/tools/base.rb +54 -3
- data/lib/rubyn_code/tools/bash.rb +16 -34
- data/lib/rubyn_code/tools/bundle_add.rb +10 -12
- data/lib/rubyn_code/tools/bundle_install.rb +9 -11
- data/lib/rubyn_code/tools/compact.rb +10 -9
- data/lib/rubyn_code/tools/db_migrate.rb +17 -15
- data/lib/rubyn_code/tools/edit_file.rb +12 -12
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/git_commit.rb +29 -34
- data/lib/rubyn_code/tools/git_diff.rb +17 -18
- data/lib/rubyn_code/tools/git_log.rb +17 -19
- data/lib/rubyn_code/tools/git_status.rb +18 -20
- data/lib/rubyn_code/tools/glob.rb +7 -9
- data/lib/rubyn_code/tools/grep.rb +11 -9
- data/lib/rubyn_code/tools/load_skill.rb +7 -7
- data/lib/rubyn_code/tools/memory_search.rb +13 -12
- data/lib/rubyn_code/tools/memory_write.rb +14 -12
- data/lib/rubyn_code/tools/rails_generate.rb +16 -16
- data/lib/rubyn_code/tools/read_file.rb +8 -7
- data/lib/rubyn_code/tools/read_inbox.rb +5 -5
- data/lib/rubyn_code/tools/registry.rb +2 -2
- data/lib/rubyn_code/tools/review_pr.rb +55 -55
- data/lib/rubyn_code/tools/run_specs.rb +20 -19
- data/lib/rubyn_code/tools/schema.rb +9 -11
- data/lib/rubyn_code/tools/send_message.rb +10 -10
- data/lib/rubyn_code/tools/spawn_agent.rb +51 -23
- data/lib/rubyn_code/tools/spawn_teammate.rb +21 -21
- data/lib/rubyn_code/tools/task.rb +28 -28
- data/lib/rubyn_code/tools/web_fetch.rb +46 -31
- data/lib/rubyn_code/tools/web_search.rb +64 -66
- data/lib/rubyn_code/tools/write_file.rb +7 -6
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +136 -105
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
26
|
-
CREATE INDEX IF NOT EXISTS
|
|
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
|
@@ -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:
|
|
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:
|
|
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,
|
|
41
|
+
def add_tool_result(tool_use_id, _tool_name, output, is_error: false)
|
|
42
42
|
result_block = {
|
|
43
|
-
type:
|
|
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] ==
|
|
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:
|
|
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] ==
|
|
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] !=
|
|
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:
|
|
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
|
|
158
|
-
{ type:
|
|
159
|
-
when
|
|
160
|
-
{ type:
|
|
161
|
-
when
|
|
162
|
-
h = { type:
|
|
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] ==
|
|
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] ==
|
|
238
|
+
msg[:content].all? { |b| b.is_a?(Hash) && b[:type] == 'tool_result' }
|
|
190
239
|
end
|
|
191
240
|
end
|
|
192
241
|
end
|