rubyn-code 0.5.1 → 0.8.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 +137 -4
- data/db/migrations/014_multi_agent_upgrade.rb +79 -0
- data/lib/rubyn_code/agent/conversation.rb +100 -4
- data/lib/rubyn_code/agent/dynamic_tool_schema.rb +1 -1
- data/lib/rubyn_code/agent/llm_caller.rb +7 -3
- data/lib/rubyn_code/agent/loop.rb +106 -12
- data/lib/rubyn_code/agent/response_parser.rb +8 -0
- data/lib/rubyn_code/agent/system_prompt_builder.rb +40 -2
- data/lib/rubyn_code/agent/tool_processor.rb +13 -1
- data/lib/rubyn_code/auth/oauth.rb +1 -1
- data/lib/rubyn_code/auth/token_store.rb +49 -4
- data/lib/rubyn_code/autonomous/daemon.rb +1 -1
- data/lib/rubyn_code/checkpoint/hook.rb +26 -0
- data/lib/rubyn_code/checkpoint/manager.rb +109 -0
- data/lib/rubyn_code/chisel/debt.rb +65 -0
- data/lib/rubyn_code/chisel/inspection.rb +93 -0
- data/lib/rubyn_code/chisel.rb +127 -0
- data/lib/rubyn_code/cli/commands/agents.rb +31 -0
- data/lib/rubyn_code/cli/commands/chisel.rb +52 -0
- data/lib/rubyn_code/cli/commands/chisel_audit.rb +19 -0
- data/lib/rubyn_code/cli/commands/chisel_debt.rb +28 -0
- data/lib/rubyn_code/cli/commands/chisel_gain.rb +30 -0
- data/lib/rubyn_code/cli/commands/chisel_review.rb +19 -0
- data/lib/rubyn_code/cli/commands/command_template.rb +50 -0
- data/lib/rubyn_code/cli/commands/context.rb +30 -1
- data/lib/rubyn_code/cli/commands/custom_command.rb +84 -0
- data/lib/rubyn_code/cli/commands/custom_loader.rb +100 -0
- data/lib/rubyn_code/cli/commands/effort.rb +47 -0
- data/lib/rubyn_code/cli/commands/export.rb +174 -0
- data/lib/rubyn_code/cli/commands/goal.rb +87 -0
- data/lib/rubyn_code/cli/commands/learning.rb +62 -0
- data/lib/rubyn_code/cli/commands/loop.rb +58 -0
- data/lib/rubyn_code/cli/commands/mcp.rb +49 -12
- data/lib/rubyn_code/cli/commands/megaplan.rb +1 -1
- data/lib/rubyn_code/cli/commands/registry.rb +14 -9
- data/lib/rubyn_code/cli/commands/resume.rb +97 -26
- data/lib/rubyn_code/cli/commands/rewind.rb +65 -0
- data/lib/rubyn_code/cli/commands/think.rb +47 -0
- data/lib/rubyn_code/cli/first_run.rb +1 -1
- data/lib/rubyn_code/cli/loop_runner.rb +98 -0
- data/lib/rubyn_code/cli/mention_expander.rb +111 -0
- data/lib/rubyn_code/cli/renderer.rb +3 -2
- data/lib/rubyn_code/cli/repl.rb +68 -15
- data/lib/rubyn_code/cli/repl_commands.rb +76 -2
- data/lib/rubyn_code/cli/repl_setup.rb +17 -7
- data/lib/rubyn_code/cli/stream_formatter.rb +3 -2
- data/lib/rubyn_code/cli/version_check.rb +10 -3
- data/lib/rubyn_code/config/defaults.rb +14 -1
- data/lib/rubyn_code/config/schema.json +9 -0
- data/lib/rubyn_code/config/settings.rb +19 -2
- data/lib/rubyn_code/context/auto_compact.rb +1 -1
- data/lib/rubyn_code/context/manager.rb +29 -12
- data/lib/rubyn_code/context/manual_compact.rb +1 -1
- data/lib/rubyn_code/debug.rb +11 -5
- data/lib/rubyn_code/goal/evaluator.rb +95 -0
- data/lib/rubyn_code/hooks/event_map.rb +56 -0
- data/lib/rubyn_code/hooks/external_dispatcher.rb +199 -0
- data/lib/rubyn_code/hooks/goal_hook.rb +88 -0
- data/lib/rubyn_code/hooks/response.rb +83 -0
- data/lib/rubyn_code/hooks/runner.rb +61 -3
- data/lib/rubyn_code/hooks/settings_json_loader.rb +109 -0
- data/lib/rubyn_code/hooks/subprocess_executor.rb +116 -0
- data/lib/rubyn_code/ide/handlers/plan_interview_answer_handler.rb +13 -13
- data/lib/rubyn_code/ide/handlers/plan_interview_cancel_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/plan_interview_start_handler.rb +10 -10
- data/lib/rubyn_code/ide/handlers/plan_propose_handler.rb +1 -1
- data/lib/rubyn_code/ide/handlers/prompt_handler.rb +9 -1
- data/lib/rubyn_code/ide/handlers/recover_ci_handler.rb +27 -16
- data/lib/rubyn_code/ide/handlers/session_resume_handler.rb +1 -1
- data/lib/rubyn_code/index/codebase_index.rb +103 -4
- data/lib/rubyn_code/index/prism_extractor.rb +82 -0
- data/lib/rubyn_code/learning/injector.rb +1 -2
- data/lib/rubyn_code/learning/porter.rb +129 -0
- data/lib/rubyn_code/llm/adapters/anthropic.rb +168 -29
- data/lib/rubyn_code/llm/adapters/anthropic_streaming.rb +13 -0
- data/lib/rubyn_code/llm/adapters/base.rb +2 -1
- data/lib/rubyn_code/llm/adapters/openai.rb +2 -2
- data/lib/rubyn_code/llm/adapters/openai_message_translator.rb +21 -0
- data/lib/rubyn_code/llm/adapters/prompt_caching.rb +5 -1
- data/lib/rubyn_code/llm/client.rb +16 -3
- data/lib/rubyn_code/llm/image_reader.rb +60 -0
- data/lib/rubyn_code/llm/message_builder.rb +21 -1
- data/lib/rubyn_code/llm/model_router.rb +4 -4
- data/lib/rubyn_code/mcp/client.rb +59 -0
- data/lib/rubyn_code/mcp/discovery.rb +93 -0
- data/lib/rubyn_code/mcp/server_extras_bridge.rb +110 -0
- data/lib/rubyn_code/mcp/sse_transport.rb +2 -1
- data/lib/rubyn_code/mcp/tool_bridge.rb +16 -14
- data/lib/rubyn_code/megaplan/ci_recovery.rb +3 -3
- data/lib/rubyn_code/megaplan/interview_session.rb +8 -3
- data/lib/rubyn_code/megaplan/plan_proposer.rb +3 -3
- data/lib/rubyn_code/memory/search.rb +9 -5
- data/lib/rubyn_code/memory/session_persistence.rb +159 -21
- data/lib/rubyn_code/observability/cost_calculator.rb +8 -3
- data/lib/rubyn_code/output/diff_renderer.rb +62 -7
- data/lib/rubyn_code/protocols/RUBYN.md +0 -3
- data/lib/rubyn_code/skills/auto_suggest.rb +70 -2
- data/lib/rubyn_code/skills/registry_client.rb +4 -3
- data/lib/rubyn_code/sub_agents/agent_type.rb +17 -0
- data/lib/rubyn_code/sub_agents/catalog.rb +124 -0
- data/lib/rubyn_code/tasks/models.rb +0 -16
- data/lib/rubyn_code/teams/agent_registry.rb +120 -0
- data/lib/rubyn_code/teams/mailbox.rb +99 -10
- data/lib/rubyn_code/teams/manager.rb +83 -5
- data/lib/rubyn_code/teams/teammate.rb +3 -14
- data/lib/rubyn_code/tools/RUBYN.md +2 -2
- data/lib/rubyn_code/tools/ask_user.rb +15 -1
- data/lib/rubyn_code/tools/bash.rb +3 -3
- data/lib/rubyn_code/tools/code_graph.rb +134 -0
- data/lib/rubyn_code/tools/executor.rb +9 -4
- data/lib/rubyn_code/tools/spawn_agent.rb +47 -62
- data/lib/rubyn_code/tools/spawn_teammate.rb +7 -2
- data/lib/rubyn_code/tools/todo_store.rb +55 -0
- data/lib/rubyn_code/tools/todo_write.rb +88 -0
- data/lib/rubyn_code/tools/web_fetch.rb +1 -1
- data/lib/rubyn_code/tools/web_search.rb +4 -1
- data/lib/rubyn_code/version.rb +1 -1
- data/lib/rubyn_code.rb +57 -9
- data/skills/rubyn_self_test.md +462 -14
- data/skills/self_test/chisel_smoke.rb +84 -0
- data/skills/self_test/fixtures/chisel_sample.rb +64 -0
- metadata +45 -6
- data/lib/rubyn_code/context/context_budget.rb +0 -183
- data/lib/rubyn_code/context/schema_filter.rb +0 -64
- data/lib/rubyn_code/learning/shortcut.rb +0 -95
- data/lib/rubyn_code/llm/streaming.rb +0 -10
- data/lib/rubyn_code/protocols/plan_approval.rb +0 -72
|
@@ -5,7 +5,7 @@ module RubynCode
|
|
|
5
5
|
class Executor
|
|
6
6
|
attr_reader :project_root, :output_compressor, :file_cache
|
|
7
7
|
attr_accessor :llm_client, :background_worker, :on_agent_status, :db, :ask_user_callback,
|
|
8
|
-
:codebase_index, :ide_client
|
|
8
|
+
:codebase_index, :ide_client, :todo_store
|
|
9
9
|
|
|
10
10
|
def initialize(project_root:, ide_client: nil)
|
|
11
11
|
@project_root = File.expand_path(project_root)
|
|
@@ -56,12 +56,17 @@ module RubynCode
|
|
|
56
56
|
|
|
57
57
|
def build_tool(tool_name)
|
|
58
58
|
tool_class = Registry.get(tool_name)
|
|
59
|
-
# IDE-aware tools accept an ide_client parameter.
|
|
60
|
-
|
|
59
|
+
# IDE-aware tools accept an ide_client parameter. Inspect #initialize,
|
|
60
|
+
# not .method(:new) — the latter reports Class#new's own [[:rest]]
|
|
61
|
+
# signature and never forwards to the constructor.
|
|
62
|
+
if @ide_client && tool_class.instance_method(:initialize).parameters.any? { |_, name| name == :ide_client }
|
|
61
63
|
tool = tool_class.new(project_root: project_root, ide_client: @ide_client)
|
|
62
64
|
else
|
|
63
65
|
tool = tool_class.new(project_root: project_root)
|
|
64
66
|
end
|
|
67
|
+
if @todo_store && tool_class.instance_method(:initialize).parameters.any? { |_, name| name == :store }
|
|
68
|
+
tool.instance_variable_set(:@store, @todo_store)
|
|
69
|
+
end
|
|
65
70
|
inject_dependencies(tool, tool_name)
|
|
66
71
|
tool
|
|
67
72
|
end
|
|
@@ -140,7 +145,7 @@ module RubynCode
|
|
|
140
145
|
path = resolve_cache_path(params)
|
|
141
146
|
return unless path&.end_with?('.rb')
|
|
142
147
|
|
|
143
|
-
@codebase_index.
|
|
148
|
+
@codebase_index.update_file!(path)
|
|
144
149
|
rescue StandardError => e
|
|
145
150
|
RubynCode::Debug.warn("CodebaseIndex incremental update failed: #{e.message}")
|
|
146
151
|
end
|
|
@@ -9,8 +9,9 @@ module RubynCode
|
|
|
9
9
|
TOOL_NAME = 'spawn_agent'
|
|
10
10
|
DESCRIPTION = 'Spawn an isolated sub-agent to handle a task. The sub-agent gets its own ' \
|
|
11
11
|
"fresh context, works independently, and returns only a summary. Use 'explore' " \
|
|
12
|
-
"type for research/reading, 'worker' type for writing code/files
|
|
13
|
-
'
|
|
12
|
+
"type for research/reading, 'worker' type for writing code/files, or the name " \
|
|
13
|
+
'of any custom agent defined in .rubyn-code/agents/. The sub-agent shares the ' \
|
|
14
|
+
'filesystem but not your conversation.'
|
|
14
15
|
PARAMETERS = {
|
|
15
16
|
prompt: {
|
|
16
17
|
type: :string,
|
|
@@ -19,76 +20,73 @@ module RubynCode
|
|
|
19
20
|
},
|
|
20
21
|
agent_type: {
|
|
21
22
|
type: :string,
|
|
22
|
-
description: "
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
description: "Agent type: 'explore' (read-only), 'worker' (full write access), or a " \
|
|
24
|
+
'custom agent name from .rubyn-code/agents/. Default: explore',
|
|
25
|
+
required: false
|
|
25
26
|
}
|
|
26
27
|
}.freeze
|
|
27
28
|
RISK_LEVEL = :execute
|
|
28
29
|
|
|
30
|
+
READ_TOOLS = %w[read_file glob grep bash load_skill memory_search].freeze
|
|
31
|
+
BLOCKED_TOOLS = %w[spawn_agent send_message read_inbox compact memory_write].freeze
|
|
32
|
+
|
|
29
33
|
# These get injected by the executor or the REPL
|
|
30
34
|
attr_writer :llm_client, :on_status
|
|
31
35
|
|
|
32
36
|
def execute(prompt:, agent_type: 'explore')
|
|
33
|
-
|
|
37
|
+
agent = resolve_agent(agent_type)
|
|
34
38
|
callback = @on_status || method(:default_status)
|
|
35
39
|
@tool_count = 0
|
|
36
40
|
|
|
37
|
-
callback.call(:started, "Spawning #{
|
|
41
|
+
callback.call(:started, "Spawning #{agent.name} agent...")
|
|
38
42
|
|
|
39
|
-
tools =
|
|
40
|
-
result, hit_limit = run_sub_agent(
|
|
41
|
-
prompt: prompt, tools: tools, type: type, callback: callback
|
|
42
|
-
)
|
|
43
|
+
tools = tools_for(agent)
|
|
44
|
+
result, hit_limit = run_sub_agent(prompt: prompt, tools: tools, agent: agent, callback: callback)
|
|
43
45
|
|
|
44
46
|
callback.call(:done, "Agent finished (#{@tool_count} tool calls).")
|
|
45
47
|
|
|
46
48
|
summary = RubynCode::SubAgents::Summarizer.call(result, max_length: 3000)
|
|
47
|
-
format_agent_result(
|
|
49
|
+
format_agent_result(agent.name, summary, hit_limit)
|
|
48
50
|
end
|
|
49
51
|
|
|
50
52
|
private
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
# Resolve the requested type via the catalog, falling back to explore
|
|
55
|
+
# for an unknown name so a typo degrades gracefully instead of erroring.
|
|
56
|
+
def resolve_agent(agent_type)
|
|
57
|
+
catalog = RubynCode::SubAgents::Catalog.new(project_root: project_root)
|
|
58
|
+
catalog.get(agent_type) || catalog.get('explore')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_agent_result(name, summary, hit_limit)
|
|
53
62
|
if hit_limit
|
|
54
|
-
"## Sub-Agent Result (#{
|
|
63
|
+
"## Sub-Agent Result (#{name}) — INCOMPLETE (reached #{@tool_count} tool calls)\n\n" \
|
|
55
64
|
'The sub-agent ran out of turns before finishing. Here is what it accomplished so far:' \
|
|
56
65
|
"\n\n#{summary}"
|
|
57
66
|
else
|
|
58
|
-
"## Sub-Agent Result (#{
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def max_iterations_for(type)
|
|
63
|
-
if type == :explore
|
|
64
|
-
Config::Defaults::MAX_EXPLORE_AGENT_ITERATIONS
|
|
65
|
-
else
|
|
66
|
-
Config::Defaults::MAX_SUB_AGENT_ITERATIONS
|
|
67
|
+
"## Sub-Agent Result (#{name})\n\n#{summary}"
|
|
67
68
|
end
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
# Returns [result_text, hit_limit] tuple
|
|
71
|
-
def run_sub_agent(prompt:, tools:,
|
|
72
|
+
def run_sub_agent(prompt:, tools:, agent:, callback:)
|
|
72
73
|
conversation = RubynCode::Agent::Conversation.new
|
|
73
74
|
conversation.add_user_message(prompt)
|
|
74
75
|
|
|
75
|
-
max_iterations = max_iterations_for(type)
|
|
76
76
|
iteration = 0
|
|
77
77
|
last_text = nil
|
|
78
78
|
|
|
79
79
|
loop do
|
|
80
|
-
return finish_at_limit(conversation,
|
|
80
|
+
return finish_at_limit(conversation, agent, last_text) if iteration >= agent.max_iterations
|
|
81
81
|
|
|
82
|
-
last_text, done = process_iteration(
|
|
83
|
-
conversation, tools, type, callback, last_text
|
|
84
|
-
)
|
|
82
|
+
last_text, done = process_iteration(conversation, tools, agent, callback, last_text)
|
|
85
83
|
return [last_text || '', false] if done
|
|
86
84
|
|
|
87
85
|
iteration += 1
|
|
88
86
|
end
|
|
89
87
|
end
|
|
90
88
|
|
|
91
|
-
def finish_at_limit(conversation,
|
|
89
|
+
def finish_at_limit(conversation, agent, last_text)
|
|
92
90
|
conversation.add_user_message(
|
|
93
91
|
'You have reached your turn limit. Summarize everything you found or ' \
|
|
94
92
|
'accomplished so far. Be thorough — this is your last chance to report back.'
|
|
@@ -96,17 +94,17 @@ module RubynCode
|
|
|
96
94
|
response = @llm_client.chat(
|
|
97
95
|
messages: conversation.to_api_format,
|
|
98
96
|
tools: [],
|
|
99
|
-
system:
|
|
97
|
+
system: agent.system_prompt
|
|
100
98
|
)
|
|
101
99
|
summary = extract_text(response)
|
|
102
100
|
[summary.empty? ? (last_text || '') : summary, true]
|
|
103
101
|
end
|
|
104
102
|
|
|
105
|
-
def process_iteration(conversation, tools,
|
|
103
|
+
def process_iteration(conversation, tools, agent, callback, last_text)
|
|
106
104
|
response = @llm_client.chat(
|
|
107
105
|
messages: conversation.to_api_format,
|
|
108
106
|
tools: tools,
|
|
109
|
-
system:
|
|
107
|
+
system: agent.system_prompt
|
|
110
108
|
)
|
|
111
109
|
|
|
112
110
|
content = response_content(response)
|
|
@@ -117,22 +115,22 @@ module RubynCode
|
|
|
117
115
|
conversation.add_assistant_message(content)
|
|
118
116
|
return [last_text, true] if tool_calls.empty?
|
|
119
117
|
|
|
120
|
-
execute_sub_agent_tools(tool_calls, conversation,
|
|
118
|
+
execute_sub_agent_tools(tool_calls, conversation, agent, callback)
|
|
121
119
|
[last_text, false]
|
|
122
120
|
end
|
|
123
121
|
|
|
124
|
-
def execute_sub_agent_tools(tool_calls, conversation,
|
|
122
|
+
def execute_sub_agent_tools(tool_calls, conversation, agent, callback)
|
|
125
123
|
tool_calls.each do |tc|
|
|
126
124
|
name, input, id = extract_tool_call(tc)
|
|
127
125
|
@tool_count += 1
|
|
128
126
|
callback.call(:tool, name.to_s)
|
|
129
127
|
|
|
130
|
-
run_single_tool(name, input, id, conversation,
|
|
128
|
+
run_single_tool(name, input, id, conversation, agent)
|
|
131
129
|
end
|
|
132
130
|
end
|
|
133
131
|
|
|
134
|
-
def run_single_tool(name, input, id, conversation,
|
|
135
|
-
if
|
|
132
|
+
def run_single_tool(name, input, id, conversation, agent)
|
|
133
|
+
if name == 'spawn_agent'
|
|
136
134
|
conversation.add_tool_result(
|
|
137
135
|
id, name, 'Error: Sub-agents cannot spawn other agents.', is_error: true
|
|
138
136
|
)
|
|
@@ -140,9 +138,9 @@ module RubynCode
|
|
|
140
138
|
end
|
|
141
139
|
|
|
142
140
|
tool_class = RubynCode::Tools::Registry.get(name)
|
|
143
|
-
if
|
|
141
|
+
if agent.read_only? && tool_class.risk_level != :read
|
|
144
142
|
conversation.add_tool_result(
|
|
145
|
-
id, name,
|
|
143
|
+
id, name, "Error: #{agent.name} agents can only use read-only tools.", is_error: true
|
|
146
144
|
)
|
|
147
145
|
return
|
|
148
146
|
end
|
|
@@ -154,31 +152,18 @@ module RubynCode
|
|
|
154
152
|
conversation.add_tool_result(id, name, "Error: #{e.message}", is_error: true)
|
|
155
153
|
end
|
|
156
154
|
|
|
157
|
-
|
|
155
|
+
# The tool allowlist: explicit list from a custom agent, else the
|
|
156
|
+
# access-based default (read-only set or everything-minus-blocked).
|
|
157
|
+
def tools_for(agent)
|
|
158
158
|
all_tools = RubynCode::Tools::Registry.tool_definitions
|
|
159
|
-
blocked = %w[spawn_agent send_message read_inbox compact memory_write]
|
|
160
|
-
|
|
161
|
-
if type == :explore
|
|
162
|
-
read_tools = %w[read_file glob grep bash load_skill memory_search]
|
|
163
|
-
all_tools.select { |t| read_tools.include?(t[:name]) }
|
|
164
|
-
else
|
|
165
|
-
all_tools.reject { |t| blocked.include?(t[:name]) }
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
159
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
when :explore
|
|
175
|
-
"#{base}\nYou have read-only access. Search, read files, and analyze. " \
|
|
176
|
-
'Do NOT attempt to write or modify anything.'
|
|
177
|
-
when :worker
|
|
178
|
-
"#{base}\nYou have full read/write access. Make the changes needed, " \
|
|
179
|
-
'run tests if appropriate, and report what you did.'
|
|
160
|
+
if agent.tool_names && !agent.tool_names.empty?
|
|
161
|
+
allowed = agent.tool_names - BLOCKED_TOOLS
|
|
162
|
+
all_tools.select { |t| allowed.include?(t[:name]) }
|
|
163
|
+
elsif agent.read_only?
|
|
164
|
+
all_tools.select { |t| READ_TOOLS.include?(t[:name]) }
|
|
180
165
|
else
|
|
181
|
-
|
|
166
|
+
all_tools.reject { |t| BLOCKED_TOOLS.include?(t[:name]) }
|
|
182
167
|
end
|
|
183
168
|
end
|
|
184
169
|
|
|
@@ -25,13 +25,18 @@ module RubynCode
|
|
|
25
25
|
type: :string,
|
|
26
26
|
description: 'Initial task or instruction for the teammate',
|
|
27
27
|
required: true
|
|
28
|
+
},
|
|
29
|
+
parent_agent_id: {
|
|
30
|
+
type: :string,
|
|
31
|
+
description: 'ID of the parent agent spawning this teammate (for lineage tracking)',
|
|
32
|
+
required: false
|
|
28
33
|
}
|
|
29
34
|
}.freeze
|
|
30
35
|
RISK_LEVEL = :execute
|
|
31
36
|
|
|
32
37
|
attr_writer :llm_client, :on_status, :db
|
|
33
38
|
|
|
34
|
-
def execute(name:, role:, prompt:)
|
|
39
|
+
def execute(name:, role:, prompt:, parent_agent_id: nil)
|
|
35
40
|
callback = @on_status || method(:default_status)
|
|
36
41
|
|
|
37
42
|
raise Error, 'LLM client not available' unless @llm_client
|
|
@@ -40,7 +45,7 @@ module RubynCode
|
|
|
40
45
|
mailbox = Teams::Mailbox.new(@db)
|
|
41
46
|
manager = Teams::Manager.new(@db, mailbox: mailbox)
|
|
42
47
|
|
|
43
|
-
teammate = manager.spawn(name: name, role: role)
|
|
48
|
+
teammate = manager.spawn(name: name, role: role, parent_agent_id: parent_agent_id)
|
|
44
49
|
callback.call(:started, "Spawning teammate '#{name}' as #{role}...")
|
|
45
50
|
|
|
46
51
|
Thread.new do
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubynCode
|
|
4
|
+
module Tools
|
|
5
|
+
# Shared, thread-safe checklist store. The Agent::Loop owns one instance and
|
|
6
|
+
# exposes it to (a) the renderer so the user sees what's in flight, and
|
|
7
|
+
# (b) the TodoWrite tool so the model can mutate it.
|
|
8
|
+
class TodoStore
|
|
9
|
+
include MonitorMixin
|
|
10
|
+
|
|
11
|
+
Item = Data.define(:content, :status, :active_form)
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
super
|
|
15
|
+
@items = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def replace(items)
|
|
19
|
+
synchronize do
|
|
20
|
+
@items = items.map do |item|
|
|
21
|
+
Item.new(
|
|
22
|
+
content: item['content'] || item[:content],
|
|
23
|
+
status: item['status'] || item[:status],
|
|
24
|
+
active_form: (item['active_form'] || item[:active_form]).to_s
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def current
|
|
31
|
+
synchronize { @items.map(&:to_h) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def clear
|
|
35
|
+
synchronize { @items = [] }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def empty?
|
|
39
|
+
current.empty?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def render
|
|
43
|
+
current.map do |item|
|
|
44
|
+
mark =
|
|
45
|
+
case item[:status]
|
|
46
|
+
when 'completed' then '☑'
|
|
47
|
+
when 'in_progress' then '[~]'
|
|
48
|
+
else '[ ]'
|
|
49
|
+
end
|
|
50
|
+
"#{mark} #{item[:content]}"
|
|
51
|
+
end.join("\n")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'registry'
|
|
5
|
+
|
|
6
|
+
module RubynCode
|
|
7
|
+
module Tools
|
|
8
|
+
# Update the in-turn task checklist. The model uses this to keep the user
|
|
9
|
+
# informed of progress while it works. The store is shared between the
|
|
10
|
+
# Agent::Loop (which exposes it for the renderer) and this tool.
|
|
11
|
+
class TodoWrite < Base
|
|
12
|
+
TOOL_NAME = 'TodoWrite'
|
|
13
|
+
DESCRIPTION = 'Update the in-turn task checklist. ' \
|
|
14
|
+
'Use this to keep the user informed of what you plan to do, ' \
|
|
15
|
+
'what you are currently working on, and what you have finished.'
|
|
16
|
+
PARAMETERS = {
|
|
17
|
+
todos: { type: :array, required: true,
|
|
18
|
+
description: 'The full set of tasks currently on the checklist. ' \
|
|
19
|
+
'Replace the existing list with this. Each task is ' \
|
|
20
|
+
'{ "content": "...", "status": "pending|in_progress|completed", ' \
|
|
21
|
+
'"active_form": "..." }.' }
|
|
22
|
+
}.freeze
|
|
23
|
+
RISK_LEVEL = :read
|
|
24
|
+
REQUIRES_CONFIRMATION = false
|
|
25
|
+
|
|
26
|
+
VALID_STATUS = %w[pending in_progress completed].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(project_root:, store: nil)
|
|
29
|
+
super(project_root: project_root)
|
|
30
|
+
@store = store
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def execute(todos:)
|
|
34
|
+
items = Array(todos)
|
|
35
|
+
validated = []
|
|
36
|
+
|
|
37
|
+
items.each_with_index do |item, i|
|
|
38
|
+
return "TodoWrite: item #{i} is not a hash — got #{item.class}" unless item.is_a?(Hash)
|
|
39
|
+
|
|
40
|
+
content = item[:content] || item['content']
|
|
41
|
+
status = (item[:status] || item['status']).to_s
|
|
42
|
+
active_form = item[:active_form] || item['active_form']
|
|
43
|
+
|
|
44
|
+
return "TodoWrite: item #{i} missing 'content'" if content.to_s.empty?
|
|
45
|
+
unless VALID_STATUS.include?(status)
|
|
46
|
+
return "TodoWrite: item #{i} status must be one of #{VALID_STATUS.join('/')} (got #{status.inspect})"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
validated << {
|
|
50
|
+
'content' => content.to_s,
|
|
51
|
+
'status' => status,
|
|
52
|
+
'active_form' => active_form.to_s.empty? ? content.to_s : active_form.to_s
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@store&.replace(validated)
|
|
57
|
+
format(validated)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.summarize(_output, args)
|
|
61
|
+
count = Array(args[:todos] || args['todos']).size
|
|
62
|
+
if count.zero?
|
|
63
|
+
'cleared checklist'
|
|
64
|
+
else
|
|
65
|
+
"checklist: #{count} item#{'s' unless count == 1}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def format(items)
|
|
72
|
+
return 'Checklist cleared.' if items.empty?
|
|
73
|
+
|
|
74
|
+
items.map do |item|
|
|
75
|
+
mark =
|
|
76
|
+
case item['status']
|
|
77
|
+
when 'completed' then '[x]'
|
|
78
|
+
when 'in_progress' then '[~]'
|
|
79
|
+
else '[ ]'
|
|
80
|
+
end
|
|
81
|
+
"#{mark} #{item['content']}"
|
|
82
|
+
end.join("\n")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Registry.register(TodoWrite)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'faraday'
|
|
4
3
|
require_relative 'base'
|
|
5
4
|
require_relative 'registry'
|
|
6
5
|
|
|
@@ -76,6 +75,7 @@ module RubynCode
|
|
|
76
75
|
end
|
|
77
76
|
|
|
78
77
|
def build_connection
|
|
78
|
+
require 'faraday'
|
|
79
79
|
Faraday.new do |f|
|
|
80
80
|
f.options.timeout = 30
|
|
81
81
|
f.options.open_timeout = 10
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'cgi'
|
|
5
5
|
require 'json'
|
|
6
|
-
require 'faraday'
|
|
7
6
|
require_relative 'base'
|
|
8
7
|
require_relative 'registry'
|
|
9
8
|
|
|
@@ -112,6 +111,7 @@ module RubynCode
|
|
|
112
111
|
end
|
|
113
112
|
|
|
114
113
|
def brave_request(query, num_results)
|
|
114
|
+
require 'faraday'
|
|
115
115
|
Faraday.get('https://api.search.brave.com/res/v1/web/search') do |req|
|
|
116
116
|
req.params['q'] = query
|
|
117
117
|
req.params['count'] = num_results
|
|
@@ -135,6 +135,7 @@ module RubynCode
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
def tavily_request(query, num_results)
|
|
138
|
+
require 'faraday'
|
|
138
139
|
Faraday.post('https://api.tavily.com/search') do |req|
|
|
139
140
|
req.headers['Content-Type'] = 'application/json'
|
|
140
141
|
req.body = JSON.generate(
|
|
@@ -156,6 +157,7 @@ module RubynCode
|
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
def serpapi_request(query, num_results)
|
|
160
|
+
require 'faraday'
|
|
159
161
|
Faraday.get('https://serpapi.com/search.json') do |req|
|
|
160
162
|
req.params['q'] = query
|
|
161
163
|
req.params['num'] = num_results
|
|
@@ -175,6 +177,7 @@ module RubynCode
|
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
def google_request(query, num_results)
|
|
180
|
+
require 'faraday'
|
|
178
181
|
Faraday.get('https://www.googleapis.com/customsearch/v1') do |req|
|
|
179
182
|
req.params['q'] = query
|
|
180
183
|
req.params['num'] = [num_results, 10].min
|
data/lib/rubyn_code/version.rb
CHANGED
data/lib/rubyn_code.rb
CHANGED
|
@@ -16,11 +16,16 @@ module RubynCode
|
|
|
16
16
|
# rather than a successful tool call returning a string like "denied".
|
|
17
17
|
class UserDeniedError < Error; end
|
|
18
18
|
|
|
19
|
-
#
|
|
20
|
-
autoload :
|
|
19
|
+
# Chisel — opt-in "write the minimum that works" enforcement (off by default)
|
|
20
|
+
autoload :Chisel, 'rubyn_code/chisel'
|
|
21
21
|
|
|
22
|
+
# Infrastructure
|
|
22
23
|
module Config
|
|
24
|
+
autoload :Settings, 'rubyn_code/config/settings'
|
|
25
|
+
autoload :Defaults, 'rubyn_code/config/defaults'
|
|
26
|
+
autoload :ProjectConfig, 'rubyn_code/config/project_config'
|
|
23
27
|
autoload :ProjectProfile, 'rubyn_code/config/project_profile'
|
|
28
|
+
autoload :Validator, 'rubyn_code/config/validator'
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
# Database
|
|
@@ -42,6 +47,11 @@ module RubynCode
|
|
|
42
47
|
module LLM
|
|
43
48
|
autoload :Client, 'rubyn_code/llm/client'
|
|
44
49
|
autoload :MessageBuilder, 'rubyn_code/llm/message_builder'
|
|
50
|
+
autoload :TextBlock, 'rubyn_code/llm/message_builder'
|
|
51
|
+
autoload :ThinkingBlock, 'rubyn_code/llm/message_builder'
|
|
52
|
+
autoload :ImageBlock, 'rubyn_code/llm/message_builder'
|
|
53
|
+
autoload :ToolUseBlock, 'rubyn_code/llm/message_builder'
|
|
54
|
+
autoload :ImageReader, 'rubyn_code/llm/image_reader'
|
|
45
55
|
autoload :ModelRouter, 'rubyn_code/llm/model_router'
|
|
46
56
|
|
|
47
57
|
# Adapters (provider-specific implementations)
|
|
@@ -57,9 +67,6 @@ module RubynCode
|
|
|
57
67
|
autoload :OpenAICompatible, 'rubyn_code/llm/adapters/openai_compatible'
|
|
58
68
|
autoload :OpenAIMessageTranslator, 'rubyn_code/llm/adapters/openai_message_translator'
|
|
59
69
|
end
|
|
60
|
-
|
|
61
|
-
# Backward-compat: LLM::Streaming → Adapters::AnthropicStreaming
|
|
62
|
-
autoload :Streaming, 'rubyn_code/llm/streaming'
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
# Layer 1: Agent Loop
|
|
@@ -75,6 +82,7 @@ module RubynCode
|
|
|
75
82
|
module Tools
|
|
76
83
|
autoload :Base, 'rubyn_code/tools/base'
|
|
77
84
|
autoload :Registry, 'rubyn_code/tools/registry'
|
|
85
|
+
autoload :TodoStore, 'rubyn_code/tools/todo_store'
|
|
78
86
|
autoload :Schema, 'rubyn_code/tools/schema'
|
|
79
87
|
autoload :Executor, 'rubyn_code/tools/executor'
|
|
80
88
|
autoload :ReadFile, 'rubyn_code/tools/read_file'
|
|
@@ -82,6 +90,7 @@ module RubynCode
|
|
|
82
90
|
autoload :EditFile, 'rubyn_code/tools/edit_file'
|
|
83
91
|
autoload :Glob, 'rubyn_code/tools/glob'
|
|
84
92
|
autoload :Grep, 'rubyn_code/tools/grep'
|
|
93
|
+
autoload :CodeGraph, 'rubyn_code/tools/code_graph'
|
|
85
94
|
autoload :Bash, 'rubyn_code/tools/bash'
|
|
86
95
|
autoload :RailsGenerate, 'rubyn_code/tools/rails_generate'
|
|
87
96
|
autoload :DbMigrate, 'rubyn_code/tools/db_migrate'
|
|
@@ -89,6 +98,7 @@ module RubynCode
|
|
|
89
98
|
autoload :BundleInstall, 'rubyn_code/tools/bundle_install'
|
|
90
99
|
autoload :BundleAdd, 'rubyn_code/tools/bundle_add'
|
|
91
100
|
autoload :Compact, 'rubyn_code/tools/compact'
|
|
101
|
+
autoload :TodoWrite, 'rubyn_code/tools/todo_write'
|
|
92
102
|
autoload :LoadSkill, 'rubyn_code/tools/load_skill'
|
|
93
103
|
autoload :Task, 'rubyn_code/tools/task'
|
|
94
104
|
autoload :MemorySearch, 'rubyn_code/tools/memory_search'
|
|
@@ -127,8 +137,6 @@ module RubynCode
|
|
|
127
137
|
autoload :AutoCompact, 'rubyn_code/context/auto_compact'
|
|
128
138
|
autoload :ManualCompact, 'rubyn_code/context/manual_compact'
|
|
129
139
|
autoload :ContextCollapse, 'rubyn_code/context/context_collapse'
|
|
130
|
-
autoload :ContextBudget, 'rubyn_code/context/context_budget'
|
|
131
|
-
autoload :SchemaFilter, 'rubyn_code/context/schema_filter'
|
|
132
140
|
autoload :DecisionCompactor, 'rubyn_code/context/decision_compactor'
|
|
133
141
|
end
|
|
134
142
|
|
|
@@ -153,6 +161,14 @@ module RubynCode
|
|
|
153
161
|
module SubAgents
|
|
154
162
|
autoload :Runner, 'rubyn_code/sub_agents/runner'
|
|
155
163
|
autoload :Summarizer, 'rubyn_code/sub_agents/summarizer'
|
|
164
|
+
autoload :AgentType, 'rubyn_code/sub_agents/agent_type'
|
|
165
|
+
autoload :Catalog, 'rubyn_code/sub_agents/catalog'
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Checkpoints (/rewind)
|
|
169
|
+
module Checkpoint
|
|
170
|
+
autoload :Manager, 'rubyn_code/checkpoint/manager'
|
|
171
|
+
autoload :Hook, 'rubyn_code/checkpoint/hook'
|
|
156
172
|
end
|
|
157
173
|
|
|
158
174
|
# Layer 7: Tasks
|
|
@@ -181,12 +197,12 @@ module RubynCode
|
|
|
181
197
|
autoload :Manager, 'rubyn_code/teams/manager'
|
|
182
198
|
autoload :Mailbox, 'rubyn_code/teams/mailbox'
|
|
183
199
|
autoload :Teammate, 'rubyn_code/teams/teammate'
|
|
200
|
+
autoload :AgentRegistry, 'rubyn_code/teams/agent_registry'
|
|
184
201
|
end
|
|
185
202
|
|
|
186
203
|
# Layer 10: Protocols
|
|
187
204
|
module Protocols
|
|
188
205
|
autoload :ShutdownHandshake, 'rubyn_code/protocols/shutdown_handshake'
|
|
189
|
-
autoload :PlanApproval, 'rubyn_code/protocols/plan_approval'
|
|
190
206
|
autoload :InterruptHandler, 'rubyn_code/protocols/interrupt_handler'
|
|
191
207
|
end
|
|
192
208
|
|
|
@@ -222,6 +238,17 @@ module RubynCode
|
|
|
222
238
|
autoload :Runner, 'rubyn_code/hooks/runner'
|
|
223
239
|
autoload :BuiltIn, 'rubyn_code/hooks/built_in'
|
|
224
240
|
autoload :UserHooks, 'rubyn_code/hooks/user_hooks'
|
|
241
|
+
autoload :GoalHook, 'rubyn_code/hooks/goal_hook'
|
|
242
|
+
autoload :EventMap, 'rubyn_code/hooks/event_map'
|
|
243
|
+
autoload :Response, 'rubyn_code/hooks/response'
|
|
244
|
+
autoload :SettingsJsonLoader, 'rubyn_code/hooks/settings_json_loader'
|
|
245
|
+
autoload :SubprocessExecutor, 'rubyn_code/hooks/subprocess_executor'
|
|
246
|
+
autoload :ExternalDispatcher, 'rubyn_code/hooks/external_dispatcher'
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Session goals (/goal)
|
|
250
|
+
module Goal
|
|
251
|
+
autoload :Evaluator, 'rubyn_code/goal/evaluator'
|
|
225
252
|
end
|
|
226
253
|
|
|
227
254
|
# Layer 15: MCP
|
|
@@ -230,7 +257,9 @@ module RubynCode
|
|
|
230
257
|
autoload :StdioTransport, 'rubyn_code/mcp/stdio_transport'
|
|
231
258
|
autoload :SSETransport, 'rubyn_code/mcp/sse_transport'
|
|
232
259
|
autoload :ToolBridge, 'rubyn_code/mcp/tool_bridge'
|
|
260
|
+
autoload :ServerExtrasBridge, 'rubyn_code/mcp/server_extras_bridge'
|
|
233
261
|
autoload :Config, 'rubyn_code/mcp/config'
|
|
262
|
+
autoload :Discovery, 'rubyn_code/mcp/discovery'
|
|
234
263
|
end
|
|
235
264
|
|
|
236
265
|
# Layer 16: Learning
|
|
@@ -239,7 +268,7 @@ module RubynCode
|
|
|
239
268
|
autoload :Instinct, 'rubyn_code/learning/instinct'
|
|
240
269
|
autoload :InstinctMethods, 'rubyn_code/learning/instinct'
|
|
241
270
|
autoload :Injector, 'rubyn_code/learning/injector'
|
|
242
|
-
autoload :
|
|
271
|
+
autoload :Porter, 'rubyn_code/learning/porter'
|
|
243
272
|
end
|
|
244
273
|
|
|
245
274
|
# IDE (VS Code extension server)
|
|
@@ -258,6 +287,7 @@ module RubynCode
|
|
|
258
287
|
# Codebase Index
|
|
259
288
|
module Index
|
|
260
289
|
autoload :CodebaseIndex, 'rubyn_code/index/codebase_index'
|
|
290
|
+
autoload :PrismExtractor, 'rubyn_code/index/prism_extractor'
|
|
261
291
|
end
|
|
262
292
|
|
|
263
293
|
# CLI
|
|
@@ -271,6 +301,8 @@ module RubynCode
|
|
|
271
301
|
autoload :Setup, 'rubyn_code/cli/setup'
|
|
272
302
|
autoload :FirstRun, 'rubyn_code/cli/first_run'
|
|
273
303
|
autoload :DaemonRunner, 'rubyn_code/cli/daemon_runner'
|
|
304
|
+
autoload :LoopRunner, 'rubyn_code/cli/loop_runner'
|
|
305
|
+
autoload :MentionExpander, 'rubyn_code/cli/mention_expander'
|
|
274
306
|
autoload :VersionCheck, 'rubyn_code/cli/version_check'
|
|
275
307
|
|
|
276
308
|
# Slash Command System
|
|
@@ -304,6 +336,22 @@ module RubynCode
|
|
|
304
336
|
autoload :RemoveSkills, 'rubyn_code/cli/commands/remove_skills'
|
|
305
337
|
autoload :Skills, 'rubyn_code/cli/commands/skills'
|
|
306
338
|
autoload :Megaplan, 'rubyn_code/cli/commands/megaplan'
|
|
339
|
+
autoload :Goal, 'rubyn_code/cli/commands/goal'
|
|
340
|
+
autoload :Loop, 'rubyn_code/cli/commands/loop'
|
|
341
|
+
autoload :Agents, 'rubyn_code/cli/commands/agents'
|
|
342
|
+
autoload :CommandTemplate, 'rubyn_code/cli/commands/command_template'
|
|
343
|
+
autoload :CustomCommand, 'rubyn_code/cli/commands/custom_command'
|
|
344
|
+
autoload :CustomLoader, 'rubyn_code/cli/commands/custom_loader'
|
|
345
|
+
autoload :Learning, 'rubyn_code/cli/commands/learning'
|
|
346
|
+
autoload :Rewind, 'rubyn_code/cli/commands/rewind'
|
|
347
|
+
autoload :Chisel, 'rubyn_code/cli/commands/chisel'
|
|
348
|
+
autoload :ChiselReview, 'rubyn_code/cli/commands/chisel_review'
|
|
349
|
+
autoload :ChiselAudit, 'rubyn_code/cli/commands/chisel_audit'
|
|
350
|
+
autoload :ChiselDebt, 'rubyn_code/cli/commands/chisel_debt'
|
|
351
|
+
autoload :ChiselGain, 'rubyn_code/cli/commands/chisel_gain'
|
|
352
|
+
autoload :Think, 'rubyn_code/cli/commands/think'
|
|
353
|
+
autoload :Effort, 'rubyn_code/cli/commands/effort'
|
|
354
|
+
autoload :Export, 'rubyn_code/cli/commands/export'
|
|
307
355
|
end
|
|
308
356
|
end
|
|
309
357
|
|