brute 0.4.0 → 1.0.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +14 -0
  3. data/lib/brute/diff.rb +24 -0
  4. data/lib/brute/loop/agent_stream.rb +118 -0
  5. data/lib/brute/loop/agent_turn.rb +520 -0
  6. data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
  7. data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
  8. data/lib/brute/loop/step.rb +332 -0
  9. data/lib/brute/loop/tool_call_step.rb +90 -0
  10. data/lib/brute/middleware/compaction_check.rb +70 -23
  11. data/lib/brute/middleware/doom_loop_detection.rb +110 -7
  12. data/lib/brute/middleware/llm_call.rb +88 -1
  13. data/lib/brute/middleware/message_tracking.rb +140 -10
  14. data/lib/brute/middleware/otel/span.rb +32 -2
  15. data/lib/brute/middleware/otel/token_usage.rb +38 -0
  16. data/lib/brute/middleware/otel/tool_calls.rb +30 -1
  17. data/lib/brute/middleware/otel/tool_results.rb +29 -1
  18. data/lib/brute/middleware/otel.rb +5 -0
  19. data/lib/brute/middleware/reasoning_normalizer.rb +94 -0
  20. data/lib/brute/middleware/retry.rb +113 -1
  21. data/lib/brute/middleware/session_persistence.rb +46 -3
  22. data/lib/brute/middleware/token_tracking.rb +78 -0
  23. data/lib/brute/middleware/tool_error_tracking.rb +128 -1
  24. data/lib/brute/middleware/tool_use_guard.rb +64 -28
  25. data/lib/brute/middleware/tracing.rb +63 -2
  26. data/lib/brute/middleware.rb +18 -0
  27. data/lib/brute/orchestrator/turn.rb +105 -0
  28. data/lib/brute/patches/buffer_nil_guard.rb +5 -0
  29. data/lib/brute/pipeline.rb +86 -7
  30. data/lib/brute/prompts/build_switch.rb +29 -0
  31. data/lib/brute/prompts/environment.rb +43 -0
  32. data/lib/brute/prompts/identity.rb +29 -0
  33. data/lib/brute/prompts/instructions.rb +21 -0
  34. data/lib/brute/prompts/max_steps.rb +25 -0
  35. data/lib/brute/prompts/plan_reminder.rb +25 -0
  36. data/lib/brute/prompts/skills.rb +13 -0
  37. data/lib/brute/prompts.rb +28 -0
  38. data/lib/brute/providers/ollama.rb +135 -0
  39. data/lib/brute/providers/opencode_go.rb +5 -0
  40. data/lib/brute/providers/opencode_zen.rb +7 -2
  41. data/lib/brute/providers/shell.rb +2 -2
  42. data/lib/brute/providers/shell_response.rb +7 -2
  43. data/lib/brute/providers.rb +62 -0
  44. data/lib/brute/queue/base_queue.rb +222 -0
  45. data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
  46. data/lib/brute/queue/parallel_queue.rb +66 -0
  47. data/lib/brute/queue/sequential_queue.rb +63 -0
  48. data/lib/brute/{message_store.rb → store/message_store.rb} +155 -62
  49. data/lib/brute/store/session.rb +106 -0
  50. data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
  51. data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
  52. data/lib/brute/system_prompt.rb +101 -0
  53. data/lib/brute/tools/delegate.rb +59 -0
  54. data/lib/brute/tools/fs_patch.rb +54 -2
  55. data/lib/brute/tools/fs_read.rb +5 -0
  56. data/lib/brute/tools/fs_remove.rb +7 -2
  57. data/lib/brute/tools/fs_search.rb +5 -0
  58. data/lib/brute/tools/fs_undo.rb +7 -2
  59. data/lib/brute/tools/fs_write.rb +40 -2
  60. data/lib/brute/tools/net_fetch.rb +5 -0
  61. data/lib/brute/tools/question.rb +5 -0
  62. data/lib/brute/tools/shell.rb +5 -0
  63. data/lib/brute/tools/todo_read.rb +6 -1
  64. data/lib/brute/tools/todo_write.rb +6 -1
  65. data/lib/brute/tools.rb +31 -0
  66. data/lib/brute/version.rb +1 -1
  67. data/lib/brute.rb +40 -204
  68. metadata +31 -20
  69. data/lib/brute/agent_stream.rb +0 -63
  70. data/lib/brute/hooks.rb +0 -84
  71. data/lib/brute/orchestrator.rb +0 -391
  72. data/lib/brute/session.rb +0 -161
data/lib/brute/session.rb DELETED
@@ -1,161 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "fileutils"
5
- require "securerandom"
6
-
7
- module Brute
8
- # Manages session persistence. Each session is a conversation that can be
9
- # saved to disk and resumed later.
10
- #
11
- # New directory-based layout (per-session directory):
12
- #
13
- # ~/.brute/sessions/{session-id}/
14
- # session.meta.json # session metadata
15
- # context.json # llm.rb context blob (for resumption)
16
- # msg_0001.json # structured messages (OpenCode format)
17
- # msg_0002.json
18
- # ...
19
- #
20
- # Also supports the legacy flat layout for reading:
21
- #
22
- # ~/.brute/sessions/{session-id}.json
23
- # ~/.brute/sessions/{session-id}.meta.json
24
- #
25
- class Session
26
- attr_reader :id, :title, :path
27
-
28
- def initialize(id: nil, dir: nil)
29
- @id = id || SecureRandom.uuid
30
- @base_dir = dir || File.join(Dir.home, ".brute", "sessions")
31
- @session_dir = File.join(@base_dir, @id)
32
- @path = File.join(@session_dir, "context.json")
33
- @title = nil
34
- @metadata = {}
35
- FileUtils.mkdir_p(@session_dir)
36
-
37
- # Check for legacy flat-file layout and migrate path if present
38
- @legacy_path = File.join(@base_dir, "#{@id}.json")
39
- @legacy_meta = File.join(@base_dir, "#{@id}.meta.json")
40
- end
41
-
42
- # Returns a MessageStore for this session's structured messages.
43
- def message_store
44
- @message_store ||= MessageStore.new(session_id: @id, dir: @session_dir)
45
- end
46
-
47
- # Save a context to this session.
48
- def save(context, title: nil, metadata: {})
49
- @title = title if title
50
- @metadata.merge!(metadata)
51
-
52
- # Use llm.rb's built-in serialization for context (used for resumption)
53
- context.save(path: @path)
54
-
55
- # Write metadata sidecar
56
- save_meta
57
- end
58
-
59
- # Restore a context from this session.
60
- # Returns true if restored successfully, false if no session file found.
61
- def restore(context)
62
- # Try new layout first, then legacy
63
- ctx_path = if File.exist?(@path)
64
- @path
65
- elsif File.exist?(@legacy_path)
66
- @legacy_path
67
- end
68
-
69
- return false unless ctx_path
70
-
71
- context.restore(path: ctx_path)
72
-
73
- # Load metadata
74
- load_meta
75
-
76
- true
77
- end
78
-
79
- # List all saved sessions, newest first.
80
- # Scans both new directory-based layout and legacy flat files.
81
- def self.list(dir: nil)
82
- dir ||= File.join(Dir.home, ".brute", "sessions")
83
- return [] unless File.directory?(dir)
84
-
85
- sessions = {}
86
-
87
- # New layout: {id}/session.meta.json
88
- Dir.glob(File.join(dir, "*", "session.meta.json")).each do |meta_path|
89
- data = JSON.parse(File.read(meta_path), symbolize_names: true)
90
- id = data[:id]
91
- next unless id
92
- sessions[id] = {
93
- id: id,
94
- title: data[:title],
95
- saved_at: data[:saved_at],
96
- path: File.join(File.dirname(meta_path), "context.json"),
97
- }
98
- end
99
-
100
- # Legacy layout: {id}.meta.json (only if not already found)
101
- Dir.glob(File.join(dir, "*.meta.json")).each do |meta_path|
102
- # Skip files inside session subdirectories
103
- next if meta_path.include?("/session.meta.json")
104
- data = JSON.parse(File.read(meta_path), symbolize_names: true)
105
- id = data[:id]
106
- next unless id
107
- next if sessions.key?(id) # new layout takes precedence
108
- sessions[id] = {
109
- id: id,
110
- title: data[:title],
111
- saved_at: data[:saved_at],
112
- path: meta_path.sub(/\.meta\.json$/, ".json"),
113
- }
114
- end
115
-
116
- sessions.values.sort_by { |s| s[:saved_at] || "" }.reverse
117
- end
118
-
119
- # Delete a session from disk (both new and legacy layouts).
120
- def delete
121
- # New layout: remove the whole directory
122
- FileUtils.rm_rf(@session_dir) if File.directory?(@session_dir)
123
-
124
- # Legacy layout: remove flat files
125
- File.delete(@legacy_path) if File.exist?(@legacy_path)
126
- File.delete(@legacy_meta) if File.exist?(@legacy_meta)
127
- end
128
-
129
- private
130
-
131
- def meta_path
132
- File.join(@session_dir, "session.meta.json")
133
- end
134
-
135
- def save_meta
136
- data = {
137
- id: @id,
138
- title: @title,
139
- saved_at: Time.now.iso8601,
140
- metadata: @metadata,
141
- }
142
- FileUtils.mkdir_p(@session_dir)
143
- File.write(meta_path, JSON.pretty_generate(data))
144
- end
145
-
146
- def load_meta
147
- # Try new layout first
148
- path = if File.exist?(meta_path)
149
- meta_path
150
- elsif File.exist?(@legacy_meta)
151
- @legacy_meta
152
- end
153
-
154
- return unless path
155
-
156
- data = JSON.parse(File.read(path), symbolize_names: true)
157
- @title = data[:title]
158
- @metadata = data[:metadata] || {}
159
- end
160
- end
161
- end