brute 0.1.7 → 0.1.9
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_stream.rb +16 -1
- data/lib/brute/message_store.rb +269 -0
- data/lib/brute/middleware/compaction_check.rb +5 -2
- data/lib/brute/middleware/message_tracking.rb +209 -0
- data/lib/brute/middleware/otel/span.rb +75 -0
- data/lib/brute/middleware/otel/token_usage.rb +30 -0
- data/lib/brute/middleware/otel/tool_calls.rb +39 -0
- data/lib/brute/middleware/otel/tool_results.rb +37 -0
- data/lib/brute/middleware/otel.rb +29 -0
- data/lib/brute/middleware/tool_use_guard.rb +66 -23
- data/lib/brute/orchestrator.rb +46 -13
- data/lib/brute/prompts/autonomy.rb +21 -0
- data/lib/brute/prompts/base.rb +23 -0
- data/lib/brute/prompts/build_switch.rb +19 -0
- data/lib/brute/prompts/code_references.rb +21 -0
- data/lib/brute/prompts/code_style.rb +16 -0
- data/lib/brute/prompts/conventions.rb +20 -0
- data/lib/brute/prompts/doing_tasks.rb +11 -0
- data/lib/brute/prompts/editing_approach.rb +20 -0
- data/lib/brute/prompts/editing_constraints.rb +24 -0
- data/lib/brute/prompts/environment.rb +25 -0
- data/lib/brute/prompts/frontend_tasks.rb +21 -0
- data/lib/brute/prompts/git_safety.rb +19 -0
- data/lib/brute/prompts/identity.rb +11 -0
- data/lib/brute/prompts/instructions.rb +18 -0
- data/lib/brute/prompts/max_steps.rb +30 -0
- data/lib/brute/prompts/objectivity.rb +16 -0
- data/lib/brute/prompts/plan_reminder.rb +40 -0
- data/lib/brute/prompts/proactiveness.rb +19 -0
- data/lib/brute/prompts/security_and_safety.rb +17 -0
- data/lib/brute/prompts/skills.rb +22 -0
- data/lib/brute/prompts/task_management.rb +59 -0
- data/lib/brute/prompts/text/agents/compaction.txt +15 -0
- data/lib/brute/prompts/text/agents/explore.txt +17 -0
- data/lib/brute/prompts/text/agents/summary.txt +11 -0
- data/lib/brute/prompts/text/agents/title.txt +40 -0
- data/lib/brute/prompts/text/doing_tasks/anthropic.txt +11 -0
- data/lib/brute/prompts/text/doing_tasks/default.txt +6 -0
- data/lib/brute/prompts/text/doing_tasks/google.txt +9 -0
- data/lib/brute/prompts/text/identity/anthropic.txt +5 -0
- data/lib/brute/prompts/text/identity/default.txt +3 -0
- data/lib/brute/prompts/text/identity/google.txt +1 -0
- data/lib/brute/prompts/text/identity/openai.txt +3 -0
- data/lib/brute/prompts/text/tone_and_style/anthropic.txt +5 -0
- data/lib/brute/prompts/text/tone_and_style/default.txt +9 -0
- data/lib/brute/prompts/text/tone_and_style/google.txt +6 -0
- data/lib/brute/prompts/text/tone_and_style/openai.txt +17 -0
- data/lib/brute/prompts/text/tool_usage/anthropic.txt +16 -0
- data/lib/brute/prompts/text/tool_usage/default.txt +4 -0
- data/lib/brute/prompts/text/tool_usage/google.txt +4 -0
- data/lib/brute/prompts/tone_and_style.rb +11 -0
- data/lib/brute/prompts/tool_usage.rb +11 -0
- data/lib/brute/session.rb +109 -34
- data/lib/brute/skill.rb +118 -0
- data/lib/brute/system_prompt.rb +119 -64
- data/lib/brute/tools/question.rb +59 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +62 -2
- metadata +52 -2
data/lib/brute/skill.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Brute
|
|
6
|
+
# Discovers and loads SKILL.md files from standard directories.
|
|
7
|
+
#
|
|
8
|
+
# A skill is a markdown file with YAML frontmatter:
|
|
9
|
+
#
|
|
10
|
+
# ---
|
|
11
|
+
# name: debugging
|
|
12
|
+
# description: Systematic debugging workflow for isolating and fixing bugs
|
|
13
|
+
# ---
|
|
14
|
+
#
|
|
15
|
+
# When debugging, follow these steps...
|
|
16
|
+
#
|
|
17
|
+
# Skills are scanned from (in order):
|
|
18
|
+
# 1. .brute/skills/**/SKILL.md (project-local)
|
|
19
|
+
# 2. ~/.config/brute/skills/**/SKILL.md (global)
|
|
20
|
+
#
|
|
21
|
+
# The directory name containing SKILL.md becomes the skill name if frontmatter
|
|
22
|
+
# doesn't specify one.
|
|
23
|
+
#
|
|
24
|
+
module Skill
|
|
25
|
+
Info = Struct.new(:name, :description, :location, :content, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
FILENAME = "SKILL.md"
|
|
28
|
+
|
|
29
|
+
# Scan all skill directories and return an array of Info structs.
|
|
30
|
+
def self.all(cwd: Dir.pwd)
|
|
31
|
+
skills = {}
|
|
32
|
+
|
|
33
|
+
scan_dirs(cwd).each do |dir|
|
|
34
|
+
Dir.glob(File.join(dir, "**", FILENAME)).sort.each do |path|
|
|
35
|
+
info = load(path)
|
|
36
|
+
next unless info
|
|
37
|
+
# First found wins (project-local overrides global)
|
|
38
|
+
skills[info.name] ||= info
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
skills.values.sort_by(&:name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get a single skill by name.
|
|
46
|
+
def self.get(name, cwd: Dir.pwd)
|
|
47
|
+
all(cwd: cwd).detect { |s| s.name == name }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Format skills as XML for the system prompt.
|
|
51
|
+
def self.fmt(skills)
|
|
52
|
+
return nil if skills.empty?
|
|
53
|
+
|
|
54
|
+
lines = ["<available_skills>"]
|
|
55
|
+
skills.each do |skill|
|
|
56
|
+
lines << " <skill>"
|
|
57
|
+
lines << " <name>#{skill.name}</name>"
|
|
58
|
+
lines << " <description>#{skill.description}</description>"
|
|
59
|
+
lines << " </skill>"
|
|
60
|
+
end
|
|
61
|
+
lines << "</available_skills>"
|
|
62
|
+
lines.join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parse a SKILL.md file into an Info struct.
|
|
66
|
+
# Returns nil if the file is invalid or missing required fields.
|
|
67
|
+
def self.load(path)
|
|
68
|
+
raw = File.read(path)
|
|
69
|
+
frontmatter, content = parse_frontmatter(raw)
|
|
70
|
+
return nil unless frontmatter
|
|
71
|
+
|
|
72
|
+
name = frontmatter["name"] || File.basename(File.dirname(path))
|
|
73
|
+
description = frontmatter["description"]
|
|
74
|
+
return nil unless description && !description.strip.empty?
|
|
75
|
+
|
|
76
|
+
Info.new(
|
|
77
|
+
name: name.to_s.strip,
|
|
78
|
+
description: description.to_s.strip,
|
|
79
|
+
location: path,
|
|
80
|
+
content: content.to_s.strip,
|
|
81
|
+
)
|
|
82
|
+
rescue => e
|
|
83
|
+
warn "Failed to load skill #{path}: #{e.message}"
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Directories to scan for skills, in priority order.
|
|
88
|
+
def self.scan_dirs(cwd)
|
|
89
|
+
dirs = []
|
|
90
|
+
|
|
91
|
+
# Project-local
|
|
92
|
+
project = File.join(cwd, ".brute", "skills")
|
|
93
|
+
dirs << project if File.directory?(project)
|
|
94
|
+
|
|
95
|
+
# Global
|
|
96
|
+
global = File.join(Dir.home, ".config", "brute", "skills")
|
|
97
|
+
dirs << global if File.directory?(global)
|
|
98
|
+
|
|
99
|
+
dirs
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Split YAML frontmatter from markdown body.
|
|
103
|
+
# Returns [hash, string] or [nil, nil].
|
|
104
|
+
def self.parse_frontmatter(raw)
|
|
105
|
+
return [nil, nil] unless raw.start_with?("---")
|
|
106
|
+
|
|
107
|
+
parts = raw.split(/^---\s*$/, 3)
|
|
108
|
+
return [nil, nil] if parts.length < 3
|
|
109
|
+
|
|
110
|
+
frontmatter = YAML.safe_load(parts[1])
|
|
111
|
+
return [nil, nil] unless frontmatter.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
[frontmatter, parts[2]]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private_class_method :scan_dirs, :parse_frontmatter
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/brute/system_prompt.rb
CHANGED
|
@@ -1,88 +1,143 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Brute
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Deferred system prompt builder.
|
|
5
|
+
#
|
|
6
|
+
# The block passed to +build+ is stored — not executed — until +prepare+
|
|
7
|
+
# is called with a runtime context hash (provider_name, model_name, cwd, etc).
|
|
8
|
+
#
|
|
9
|
+
# sp = Brute::SystemPrompt.build do |prompt, ctx|
|
|
10
|
+
# prompt << Brute::Prompts::Identity.call(ctx)
|
|
11
|
+
# prompt << Brute::Prompts::ToneAndStyle.call(ctx)
|
|
12
|
+
# prompt << Brute::Prompts::Environment.call(ctx)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# result = sp.prepare(provider_name: "anthropic", model_name: "claude-sonnet-4-20250514", cwd: Dir.pwd)
|
|
16
|
+
# result.to_s # single joined string
|
|
17
|
+
# result.sections # array of strings (one per p.system call)
|
|
6
18
|
#
|
|
7
|
-
# Modeled after forgecode's SystemPrompt which composes a static agent
|
|
8
|
-
# personality block with dynamic environment/tool information.
|
|
9
19
|
class SystemPrompt
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@custom_rules = custom_rules
|
|
20
|
+
# Build a deferred system prompt. The block is stored and called later by +prepare+.
|
|
21
|
+
def self.build(&block)
|
|
22
|
+
new(block)
|
|
14
23
|
end
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Return the default system prompt. Selects the right provider stack at
|
|
26
|
+
# prepare-time, then appends conditional sections based on runtime state.
|
|
27
|
+
def self.default
|
|
28
|
+
build do |prompt, ctx|
|
|
29
|
+
# Provider-specific base stack
|
|
30
|
+
provider = ctx[:provider_name].to_s
|
|
31
|
+
STACKS.fetch(provider, STACKS["default"]).each do |mod|
|
|
32
|
+
prompt << mod.call(ctx)
|
|
33
|
+
end
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
# Conditional: agent-specific reminders
|
|
36
|
+
if ctx[:agent] == "plan"
|
|
37
|
+
prompt << Prompts::PlanReminder.call(ctx)
|
|
38
|
+
end
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
if ctx[:agent_switched] == "build"
|
|
41
|
+
prompt << Prompts::BuildSwitch.call(ctx)
|
|
42
|
+
end
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
44
|
+
if ctx[:max_steps_reached]
|
|
45
|
+
prompt << Prompts::MaxSteps.call(ctx)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
36
48
|
end
|
|
37
49
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
# Pre-configured prompt stacks per provider.
|
|
51
|
+
# Each maps a provider name to an ordered list of prompt modules.
|
|
52
|
+
STACKS = {
|
|
53
|
+
# Claude — full-featured with task management and detailed tool policy
|
|
54
|
+
"anthropic" => [
|
|
55
|
+
Prompts::Identity,
|
|
56
|
+
Prompts::ToneAndStyle,
|
|
57
|
+
Prompts::Objectivity,
|
|
58
|
+
Prompts::TaskManagement,
|
|
59
|
+
Prompts::DoingTasks,
|
|
60
|
+
Prompts::ToolUsage,
|
|
61
|
+
Prompts::Conventions,
|
|
62
|
+
Prompts::GitSafety,
|
|
63
|
+
Prompts::CodeReferences,
|
|
64
|
+
Prompts::Environment,
|
|
65
|
+
Prompts::Skills,
|
|
66
|
+
Prompts::Instructions,
|
|
67
|
+
],
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
# GPT-4 / o1 / o3 — pragmatic engineer persona, editing focus, autonomy
|
|
70
|
+
"openai" => [
|
|
71
|
+
Prompts::Identity,
|
|
72
|
+
Prompts::EditingApproach,
|
|
73
|
+
Prompts::Autonomy,
|
|
74
|
+
Prompts::EditingConstraints,
|
|
75
|
+
Prompts::FrontendTasks,
|
|
76
|
+
Prompts::ToneAndStyle,
|
|
77
|
+
Prompts::Conventions,
|
|
78
|
+
Prompts::GitSafety,
|
|
79
|
+
Prompts::CodeReferences,
|
|
80
|
+
Prompts::Environment,
|
|
81
|
+
Prompts::Skills,
|
|
82
|
+
Prompts::Instructions,
|
|
83
|
+
],
|
|
45
84
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
# Gemini — formal/structured, explicit workflows, security focus
|
|
86
|
+
"google" => [
|
|
87
|
+
Prompts::Identity,
|
|
88
|
+
Prompts::Conventions,
|
|
89
|
+
Prompts::DoingTasks,
|
|
90
|
+
Prompts::ToneAndStyle,
|
|
91
|
+
Prompts::SecurityAndSafety,
|
|
92
|
+
Prompts::ToolUsage,
|
|
93
|
+
Prompts::GitSafety,
|
|
94
|
+
Prompts::CodeReferences,
|
|
95
|
+
Prompts::Environment,
|
|
96
|
+
Prompts::Skills,
|
|
97
|
+
Prompts::Instructions,
|
|
98
|
+
],
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
100
|
+
# Fallback — conservative, concise, fewer than 4 lines
|
|
101
|
+
"default" => [
|
|
102
|
+
Prompts::Identity,
|
|
103
|
+
Prompts::ToneAndStyle,
|
|
104
|
+
Prompts::Proactiveness,
|
|
105
|
+
Prompts::Conventions,
|
|
106
|
+
Prompts::CodeStyle,
|
|
107
|
+
Prompts::DoingTasks,
|
|
108
|
+
Prompts::ToolUsage,
|
|
109
|
+
Prompts::GitSafety,
|
|
110
|
+
Prompts::CodeReferences,
|
|
111
|
+
Prompts::Environment,
|
|
112
|
+
Prompts::Skills,
|
|
113
|
+
Prompts::Instructions,
|
|
114
|
+
],
|
|
115
|
+
}.freeze
|
|
53
116
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **Use todo_write for multi-step tasks**: Break complex work into steps and track progress.
|
|
57
|
-
- **Use fs_search to find code**: Don't guess file locations — search first.
|
|
58
|
-
- **Use shell for git, tests, builds**: Run `git diff`, `git status`, test suites, etc.
|
|
59
|
-
- **Be precise with patch**: The `old_string` must match the file content exactly, including whitespace.
|
|
60
|
-
- **Prefer patch over write**: For existing files, use `patch` to change specific sections rather than rewriting the entire file.
|
|
61
|
-
- **Use undo to recover**: If a write or patch goes wrong, use `undo` to restore the previous version.
|
|
62
|
-
- **Delegate research**: Use `delegate` for complex analysis that needs focused investigation.
|
|
63
|
-
SECTION
|
|
117
|
+
def initialize(block)
|
|
118
|
+
@block = block
|
|
64
119
|
end
|
|
65
120
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- **Working directory**: #{@cwd}
|
|
73
|
-
- **OS**: #{RUBY_PLATFORM}
|
|
74
|
-
- **Ruby**: #{RUBY_VERSION}
|
|
75
|
-
- **Date**: #{Time.now.strftime("%Y-%m-%d")}
|
|
76
|
-
- **Files in cwd**: #{files.join(", ")}
|
|
77
|
-
SECTION
|
|
121
|
+
# Execute the stored block with the given context and return a Result.
|
|
122
|
+
def prepare(ctx)
|
|
123
|
+
sections = []
|
|
124
|
+
@block.call(sections, ctx)
|
|
125
|
+
Result.new(sections.compact.reject { |s| s.respond_to?(:empty?) && s.empty? })
|
|
78
126
|
end
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
128
|
+
# Immutable result of a prepared system prompt.
|
|
129
|
+
Result = Struct.new(:sections) do
|
|
130
|
+
def to_s
|
|
131
|
+
sections.join("\n\n")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def each(&block)
|
|
135
|
+
sections.each(&block)
|
|
136
|
+
end
|
|
83
137
|
|
|
84
|
-
|
|
85
|
-
|
|
138
|
+
def empty?
|
|
139
|
+
sections.empty?
|
|
140
|
+
end
|
|
86
141
|
end
|
|
87
142
|
end
|
|
88
143
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Tools
|
|
5
|
+
class Question < LLM::Tool
|
|
6
|
+
name "question"
|
|
7
|
+
description "Ask the user questions during execution. Use this to gather preferences, " \
|
|
8
|
+
"clarify ambiguous instructions, get decisions on implementation choices, or " \
|
|
9
|
+
"offer choices about direction. Users can always select \"Other\" to provide " \
|
|
10
|
+
"custom text input. Answers are returned as arrays of labels; set multiple: true " \
|
|
11
|
+
"to allow selecting more than one. If you recommend a specific option, make it " \
|
|
12
|
+
"the first option and add \"(Recommended)\" at the end of the label."
|
|
13
|
+
|
|
14
|
+
params do |s|
|
|
15
|
+
s.object(
|
|
16
|
+
questions: s.array(
|
|
17
|
+
s.object(
|
|
18
|
+
question: s.string.required,
|
|
19
|
+
header: s.string.required,
|
|
20
|
+
options: s.array(
|
|
21
|
+
s.object(
|
|
22
|
+
label: s.string.required,
|
|
23
|
+
description: s.string.required,
|
|
24
|
+
)
|
|
25
|
+
).required,
|
|
26
|
+
multiple: s.boolean,
|
|
27
|
+
)
|
|
28
|
+
).required
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(questions:)
|
|
33
|
+
handler = Thread.current[:on_question]
|
|
34
|
+
unless handler
|
|
35
|
+
return { error: true, message: "Cannot ask questions in non-interactive mode" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
queue = Queue.new
|
|
39
|
+
handler.call(questions, queue)
|
|
40
|
+
answers = queue.pop
|
|
41
|
+
|
|
42
|
+
format_answers(questions, answers)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format_answers(questions, answers)
|
|
48
|
+
pairs = questions.each_with_index.map do |q, i|
|
|
49
|
+
q = q.transform_keys(&:to_s) if q.is_a?(Hash)
|
|
50
|
+
header = q["header"]
|
|
51
|
+
answer = answers[i] || []
|
|
52
|
+
"\"#{header}\" = #{answer.join(', ')}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ response: "User answered: #{pairs.join('; ')}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -27,7 +27,32 @@ require_relative 'brute/file_mutation_queue'
|
|
|
27
27
|
require_relative 'brute/doom_loop'
|
|
28
28
|
require_relative 'brute/hooks'
|
|
29
29
|
require_relative 'brute/compactor'
|
|
30
|
+
require_relative 'brute/prompts/base'
|
|
31
|
+
require_relative 'brute/prompts/identity'
|
|
32
|
+
require_relative 'brute/prompts/tone_and_style'
|
|
33
|
+
require_relative 'brute/prompts/objectivity'
|
|
34
|
+
require_relative 'brute/prompts/task_management'
|
|
35
|
+
require_relative 'brute/prompts/doing_tasks'
|
|
36
|
+
require_relative 'brute/prompts/tool_usage'
|
|
37
|
+
require_relative 'brute/prompts/conventions'
|
|
38
|
+
require_relative 'brute/prompts/git_safety'
|
|
39
|
+
require_relative 'brute/prompts/code_references'
|
|
40
|
+
require_relative 'brute/prompts/environment'
|
|
41
|
+
require_relative 'brute/prompts/instructions'
|
|
42
|
+
require_relative 'brute/prompts/editing_approach'
|
|
43
|
+
require_relative 'brute/prompts/autonomy'
|
|
44
|
+
require_relative 'brute/prompts/editing_constraints'
|
|
45
|
+
require_relative 'brute/prompts/frontend_tasks'
|
|
46
|
+
require_relative 'brute/prompts/proactiveness'
|
|
47
|
+
require_relative 'brute/prompts/code_style'
|
|
48
|
+
require_relative 'brute/prompts/security_and_safety'
|
|
49
|
+
require_relative 'brute/prompts/skills'
|
|
50
|
+
require_relative 'brute/prompts/plan_reminder'
|
|
51
|
+
require_relative 'brute/prompts/max_steps'
|
|
52
|
+
require_relative 'brute/prompts/build_switch'
|
|
53
|
+
require_relative 'brute/skill'
|
|
30
54
|
require_relative 'brute/system_prompt'
|
|
55
|
+
require_relative 'brute/message_store'
|
|
31
56
|
require_relative 'brute/session'
|
|
32
57
|
require_relative 'brute/pipeline'
|
|
33
58
|
require_relative 'brute/agent_stream'
|
|
@@ -44,10 +69,12 @@ require_relative 'brute/middleware/doom_loop_detection'
|
|
|
44
69
|
require_relative 'brute/middleware/token_tracking'
|
|
45
70
|
require_relative 'brute/middleware/compaction_check'
|
|
46
71
|
require_relative 'brute/middleware/session_persistence'
|
|
72
|
+
require_relative 'brute/middleware/message_tracking'
|
|
47
73
|
require_relative 'brute/middleware/tracing'
|
|
48
74
|
require_relative 'brute/middleware/tool_error_tracking'
|
|
49
75
|
require_relative 'brute/middleware/reasoning_normalizer'
|
|
50
76
|
require_relative "brute/middleware/tool_use_guard"
|
|
77
|
+
require_relative "brute/middleware/otel"
|
|
51
78
|
|
|
52
79
|
# Tools
|
|
53
80
|
require_relative 'brute/tools/fs_read'
|
|
@@ -61,6 +88,7 @@ require_relative 'brute/tools/net_fetch'
|
|
|
61
88
|
require_relative 'brute/tools/todo_write'
|
|
62
89
|
require_relative 'brute/tools/todo_read'
|
|
63
90
|
require_relative 'brute/tools/delegate'
|
|
91
|
+
require_relative 'brute/tools/question'
|
|
64
92
|
|
|
65
93
|
# Orchestrator (depends on tools, middleware, and infrastructure)
|
|
66
94
|
require_relative 'brute/orchestrator'
|
|
@@ -78,7 +106,8 @@ module Brute
|
|
|
78
106
|
Tools::NetFetch,
|
|
79
107
|
Tools::TodoWrite,
|
|
80
108
|
Tools::TodoRead,
|
|
81
|
-
Tools::Delegate
|
|
109
|
+
Tools::Delegate,
|
|
110
|
+
Tools::Question
|
|
82
111
|
].freeze
|
|
83
112
|
|
|
84
113
|
# Default provider, resolved from environment.
|
|
@@ -91,13 +120,15 @@ module Brute
|
|
|
91
120
|
end
|
|
92
121
|
|
|
93
122
|
# Create a new orchestrator with sensible defaults.
|
|
94
|
-
def self.agent(cwd: Dir.pwd, tools: TOOLS, session: nil, reasoning: {}, **callbacks)
|
|
123
|
+
def self.agent(cwd: Dir.pwd, model: nil, tools: TOOLS, session: nil, reasoning: {}, agent_name: nil, **callbacks)
|
|
95
124
|
Orchestrator.new(
|
|
96
125
|
provider: provider,
|
|
126
|
+
model: model,
|
|
97
127
|
tools: tools,
|
|
98
128
|
cwd: cwd,
|
|
99
129
|
session: session,
|
|
100
130
|
reasoning: reasoning,
|
|
131
|
+
agent_name: agent_name,
|
|
101
132
|
**callbacks
|
|
102
133
|
)
|
|
103
134
|
end
|
|
@@ -111,6 +142,35 @@ module Brute
|
|
|
111
142
|
'xai' => ->(key) { LLM.xai(key: key) }
|
|
112
143
|
}.freeze
|
|
113
144
|
|
|
145
|
+
# List provider names that have API keys configured in the environment.
|
|
146
|
+
def self.configured_providers
|
|
147
|
+
PROVIDERS.keys.select { |name| api_key_for(name) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Build a provider instance for the given name using available API keys.
|
|
151
|
+
# Returns nil if no key is found.
|
|
152
|
+
def self.provider_for(name)
|
|
153
|
+
key = api_key_for(name)
|
|
154
|
+
return nil unless key
|
|
155
|
+
|
|
156
|
+
factory = PROVIDERS[name]
|
|
157
|
+
return nil unless factory
|
|
158
|
+
|
|
159
|
+
factory.call(key)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Look up the API key for a given provider name.
|
|
163
|
+
def self.api_key_for(name)
|
|
164
|
+
# Explicit generic key always works
|
|
165
|
+
return ENV["LLM_API_KEY"] if ENV["LLM_API_KEY"]
|
|
166
|
+
|
|
167
|
+
case name
|
|
168
|
+
when "anthropic" then ENV["ANTHROPIC_API_KEY"]
|
|
169
|
+
when "openai" then ENV["OPENAI_API_KEY"]
|
|
170
|
+
when "google" then ENV["GOOGLE_API_KEY"]
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
114
174
|
# Resolve the LLM provider from environment variables.
|
|
115
175
|
#
|
|
116
176
|
# Checks in order:
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 1980-01-
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -92,10 +92,17 @@ files:
|
|
|
92
92
|
- lib/brute/doom_loop.rb
|
|
93
93
|
- lib/brute/file_mutation_queue.rb
|
|
94
94
|
- lib/brute/hooks.rb
|
|
95
|
+
- lib/brute/message_store.rb
|
|
95
96
|
- lib/brute/middleware/base.rb
|
|
96
97
|
- lib/brute/middleware/compaction_check.rb
|
|
97
98
|
- lib/brute/middleware/doom_loop_detection.rb
|
|
98
99
|
- lib/brute/middleware/llm_call.rb
|
|
100
|
+
- lib/brute/middleware/message_tracking.rb
|
|
101
|
+
- lib/brute/middleware/otel.rb
|
|
102
|
+
- lib/brute/middleware/otel/span.rb
|
|
103
|
+
- lib/brute/middleware/otel/token_usage.rb
|
|
104
|
+
- lib/brute/middleware/otel/tool_calls.rb
|
|
105
|
+
- lib/brute/middleware/otel/tool_results.rb
|
|
99
106
|
- lib/brute/middleware/reasoning_normalizer.rb
|
|
100
107
|
- lib/brute/middleware/retry.rb
|
|
101
108
|
- lib/brute/middleware/session_persistence.rb
|
|
@@ -107,7 +114,49 @@ files:
|
|
|
107
114
|
- lib/brute/patches/anthropic_tool_role.rb
|
|
108
115
|
- lib/brute/patches/buffer_nil_guard.rb
|
|
109
116
|
- lib/brute/pipeline.rb
|
|
117
|
+
- lib/brute/prompts/autonomy.rb
|
|
118
|
+
- lib/brute/prompts/base.rb
|
|
119
|
+
- lib/brute/prompts/build_switch.rb
|
|
120
|
+
- lib/brute/prompts/code_references.rb
|
|
121
|
+
- lib/brute/prompts/code_style.rb
|
|
122
|
+
- lib/brute/prompts/conventions.rb
|
|
123
|
+
- lib/brute/prompts/doing_tasks.rb
|
|
124
|
+
- lib/brute/prompts/editing_approach.rb
|
|
125
|
+
- lib/brute/prompts/editing_constraints.rb
|
|
126
|
+
- lib/brute/prompts/environment.rb
|
|
127
|
+
- lib/brute/prompts/frontend_tasks.rb
|
|
128
|
+
- lib/brute/prompts/git_safety.rb
|
|
129
|
+
- lib/brute/prompts/identity.rb
|
|
130
|
+
- lib/brute/prompts/instructions.rb
|
|
131
|
+
- lib/brute/prompts/max_steps.rb
|
|
132
|
+
- lib/brute/prompts/objectivity.rb
|
|
133
|
+
- lib/brute/prompts/plan_reminder.rb
|
|
134
|
+
- lib/brute/prompts/proactiveness.rb
|
|
135
|
+
- lib/brute/prompts/security_and_safety.rb
|
|
136
|
+
- lib/brute/prompts/skills.rb
|
|
137
|
+
- lib/brute/prompts/task_management.rb
|
|
138
|
+
- lib/brute/prompts/text/agents/compaction.txt
|
|
139
|
+
- lib/brute/prompts/text/agents/explore.txt
|
|
140
|
+
- lib/brute/prompts/text/agents/summary.txt
|
|
141
|
+
- lib/brute/prompts/text/agents/title.txt
|
|
142
|
+
- lib/brute/prompts/text/doing_tasks/anthropic.txt
|
|
143
|
+
- lib/brute/prompts/text/doing_tasks/default.txt
|
|
144
|
+
- lib/brute/prompts/text/doing_tasks/google.txt
|
|
145
|
+
- lib/brute/prompts/text/identity/anthropic.txt
|
|
146
|
+
- lib/brute/prompts/text/identity/default.txt
|
|
147
|
+
- lib/brute/prompts/text/identity/google.txt
|
|
148
|
+
- lib/brute/prompts/text/identity/openai.txt
|
|
149
|
+
- lib/brute/prompts/text/tone_and_style/anthropic.txt
|
|
150
|
+
- lib/brute/prompts/text/tone_and_style/default.txt
|
|
151
|
+
- lib/brute/prompts/text/tone_and_style/google.txt
|
|
152
|
+
- lib/brute/prompts/text/tone_and_style/openai.txt
|
|
153
|
+
- lib/brute/prompts/text/tool_usage/anthropic.txt
|
|
154
|
+
- lib/brute/prompts/text/tool_usage/default.txt
|
|
155
|
+
- lib/brute/prompts/text/tool_usage/google.txt
|
|
156
|
+
- lib/brute/prompts/tone_and_style.rb
|
|
157
|
+
- lib/brute/prompts/tool_usage.rb
|
|
110
158
|
- lib/brute/session.rb
|
|
159
|
+
- lib/brute/skill.rb
|
|
111
160
|
- lib/brute/snapshot_store.rb
|
|
112
161
|
- lib/brute/system_prompt.rb
|
|
113
162
|
- lib/brute/todo_store.rb
|
|
@@ -119,6 +168,7 @@ files:
|
|
|
119
168
|
- lib/brute/tools/fs_undo.rb
|
|
120
169
|
- lib/brute/tools/fs_write.rb
|
|
121
170
|
- lib/brute/tools/net_fetch.rb
|
|
171
|
+
- lib/brute/tools/question.rb
|
|
122
172
|
- lib/brute/tools/shell.rb
|
|
123
173
|
- lib/brute/tools/todo_read.rb
|
|
124
174
|
- lib/brute/tools/todo_write.rb
|