brute 2.0.0 → 2.0.2
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/lib/brute/agent.rb +6 -4
- data/lib/brute/middleware/005_tracing.rb +8 -0
- data/lib/brute/version.rb +1 -1
- metadata +1 -2
- data/lib/brute/tools/delegate.rb +0 -109
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e70352bd81dcba123054a2e5fb1c5bf12642aec892cf41fa48297ecbb61fea65
|
|
4
|
+
data.tar.gz: 242e0ef1477ec8201c13ce5ec535bd45abbad9dca4252c88366477487a3c0913
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1999c090d40c1cb55d2e64152a8c633d364a461c84ac337f2b80c7d1151fd732c18075ddcc0709001949dbafa098d2707485fdf9e812e2f9eb485494607c8ce9
|
|
7
|
+
data.tar.gz: 6097b281be949bc6cf52934952ddac860da082045811a8a167883071b6d90946ddd96832594589a9ae399277d638e754a404ba8fb1813e013e55b7856522b5d9
|
data/lib/brute/agent.rb
CHANGED
|
@@ -41,7 +41,8 @@ module Brute
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Run one turn against the given session. The session is mutated
|
|
44
|
-
# in place (assistant + tool messages appended)
|
|
44
|
+
# in place (assistant + tool messages appended). Returns the env
|
|
45
|
+
# hash so callers can access metadata (timing, tokens, etc.).
|
|
45
46
|
def call(session, events: NullSink.new)
|
|
46
47
|
env = {
|
|
47
48
|
messages: session,
|
|
@@ -54,19 +55,20 @@ module Brute
|
|
|
54
55
|
current_iteration: 1,
|
|
55
56
|
}
|
|
56
57
|
super(env)
|
|
57
|
-
|
|
58
|
+
env
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
test do
|
|
63
|
-
it "runs a turn and returns the session" do
|
|
64
|
+
it "runs a turn and returns the env with session in :messages" do
|
|
64
65
|
agent = Brute::Agent.new(provider: :stub) do
|
|
65
66
|
run ->(env) { env[:messages].assistant("hello") }
|
|
66
67
|
end
|
|
67
68
|
session = Brute::Session.new
|
|
68
69
|
session.user("hi")
|
|
69
|
-
agent.call(session)
|
|
70
|
+
env = agent.call(session)
|
|
71
|
+
env[:messages].should == session
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
it "passes provider/model/tools through env" do
|
|
@@ -67,6 +67,14 @@ module Brute
|
|
|
67
67
|
last_call_elapsed: elapsed
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
if response.respond_to?(:usage) && (u = response.usage)
|
|
71
|
+
env[:metadata][:tokens] = {
|
|
72
|
+
total: read_token(u, :total_tokens),
|
|
73
|
+
total_input: read_token(u, :input_tokens),
|
|
74
|
+
total_output: read_token(u, :output_tokens),
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
70
78
|
response
|
|
71
79
|
end
|
|
72
80
|
|
data/lib/brute/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
@@ -187,7 +187,6 @@ files:
|
|
|
187
187
|
- lib/brute/system_prompt.rb
|
|
188
188
|
- lib/brute/tool.rb
|
|
189
189
|
- lib/brute/tools.rb
|
|
190
|
-
- lib/brute/tools/delegate.rb
|
|
191
190
|
- lib/brute/tools/fs_patch.rb
|
|
192
191
|
- lib/brute/tools/fs_read.rb
|
|
193
192
|
- lib/brute/tools/fs_remove.rb
|
data/lib/brute/tools/delegate.rb
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/setup"
|
|
4
|
-
require "brute"
|
|
5
|
-
|
|
6
|
-
module Brute
|
|
7
|
-
module Tools
|
|
8
|
-
class Delegate < RubyLLM::Tool
|
|
9
|
-
description "Delegate a research or analysis task to a specialist sub-agent. " \
|
|
10
|
-
"The sub-agent can read files and search but cannot write or execute commands. " \
|
|
11
|
-
"Use for code analysis, understanding patterns, or gathering information."
|
|
12
|
-
|
|
13
|
-
param :task, type: 'string', desc: "A clear, detailed description of the research task", required: true
|
|
14
|
-
|
|
15
|
-
def name; "delegate"; end
|
|
16
|
-
|
|
17
|
-
MAX_ROUNDS = 10
|
|
18
|
-
|
|
19
|
-
def execute(task:)
|
|
20
|
-
provider = Brute.provider
|
|
21
|
-
llm = provider.ruby_llm_provider
|
|
22
|
-
model_id = provider.default_model
|
|
23
|
-
model = Brute::Middleware::ModelRef.new(model_id, 16_384)
|
|
24
|
-
|
|
25
|
-
sub_tools = { read: FSRead.new, fs_search: FSSearch.new }
|
|
26
|
-
|
|
27
|
-
messages = [
|
|
28
|
-
RubyLLM::Message.new(
|
|
29
|
-
role: :system,
|
|
30
|
-
content: "You are a research agent. Analyze code, explain patterns, and answer questions. " \
|
|
31
|
-
"You have read-only access to the filesystem. Be thorough and precise."
|
|
32
|
-
),
|
|
33
|
-
RubyLLM::Message.new(role: :user, content: task),
|
|
34
|
-
]
|
|
35
|
-
|
|
36
|
-
response = nil
|
|
37
|
-
MAX_ROUNDS.times do
|
|
38
|
-
response = llm.complete(messages, tools: sub_tools, temperature: nil, model: model)
|
|
39
|
-
messages << response
|
|
40
|
-
|
|
41
|
-
break unless response.tool_call?
|
|
42
|
-
|
|
43
|
-
response.tool_calls.each_value do |tc|
|
|
44
|
-
tool = sub_tools[tc.name.to_sym]
|
|
45
|
-
result = if tool
|
|
46
|
-
tool.call(tc.arguments)
|
|
47
|
-
else
|
|
48
|
-
{ error: "Unknown tool: #{tc.name}" }
|
|
49
|
-
end
|
|
50
|
-
content = result.is_a?(String) ? result : result.to_s
|
|
51
|
-
messages << RubyLLM::Message.new(role: :tool, content: content, tool_call_id: tc.id)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
{ result: extract_content(response, messages) }
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
# Safely extract text content from the sub-agent response.
|
|
61
|
-
def extract_content(response, messages)
|
|
62
|
-
text = response&.content
|
|
63
|
-
return text if text.is_a?(::String) && !text.empty?
|
|
64
|
-
|
|
65
|
-
# Fall back to last assistant text in the conversation history
|
|
66
|
-
last_assistant = messages
|
|
67
|
-
.select { |m| m.role == :assistant }
|
|
68
|
-
.reverse
|
|
69
|
-
.find { |m| m.content.is_a?(::String) && !m.content.empty? }
|
|
70
|
-
last_assistant&.content || "(sub-agent completed but produced no text response)"
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
test do
|
|
77
|
-
require_relative "../../../spec/support/mock_provider"
|
|
78
|
-
require_relative "../../../spec/support/mock_response"
|
|
79
|
-
|
|
80
|
-
delegate = Brute::Tools::Delegate.new
|
|
81
|
-
|
|
82
|
-
it "returns content when response has text" do
|
|
83
|
-
res = RubyLLM::Message.new(role: :assistant, content: "analysis complete")
|
|
84
|
-
delegate.send(:extract_content, res, []).should == "analysis complete"
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
it "falls back to last assistant text on nil content" do
|
|
88
|
-
res = RubyLLM::Message.new(role: :assistant, content: "")
|
|
89
|
-
msgs = [
|
|
90
|
-
RubyLLM::Message.new(role: :user, content: "input"),
|
|
91
|
-
RubyLLM::Message.new(role: :assistant, content: "found the answer"),
|
|
92
|
-
]
|
|
93
|
-
delegate.send(:extract_content, res, msgs).should == "found the answer"
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
it "returns fallback when no assistant messages exist" do
|
|
97
|
-
res = RubyLLM::Message.new(role: :assistant, content: "")
|
|
98
|
-
delegate.send(:extract_content, res, []).should == "(sub-agent completed but produced no text response)"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "skips assistant messages with empty content" do
|
|
102
|
-
res = RubyLLM::Message.new(role: :assistant, content: "")
|
|
103
|
-
msgs = [
|
|
104
|
-
RubyLLM::Message.new(role: :assistant, content: "real answer"),
|
|
105
|
-
RubyLLM::Message.new(role: :assistant, content: ""),
|
|
106
|
-
]
|
|
107
|
-
delegate.send(:extract_content, res, msgs).should == "real answer"
|
|
108
|
-
end
|
|
109
|
-
end
|