brute 0.1.8 → 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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent_stream.rb +4 -1
  3. data/lib/brute/middleware/message_tracking.rb +15 -1
  4. data/lib/brute/orchestrator.rb +20 -6
  5. data/lib/brute/prompts/autonomy.rb +21 -0
  6. data/lib/brute/prompts/base.rb +23 -0
  7. data/lib/brute/prompts/build_switch.rb +19 -0
  8. data/lib/brute/prompts/code_references.rb +21 -0
  9. data/lib/brute/prompts/code_style.rb +16 -0
  10. data/lib/brute/prompts/conventions.rb +20 -0
  11. data/lib/brute/prompts/doing_tasks.rb +11 -0
  12. data/lib/brute/prompts/editing_approach.rb +20 -0
  13. data/lib/brute/prompts/editing_constraints.rb +24 -0
  14. data/lib/brute/prompts/environment.rb +25 -0
  15. data/lib/brute/prompts/frontend_tasks.rb +21 -0
  16. data/lib/brute/prompts/git_safety.rb +19 -0
  17. data/lib/brute/prompts/identity.rb +11 -0
  18. data/lib/brute/prompts/instructions.rb +18 -0
  19. data/lib/brute/prompts/max_steps.rb +30 -0
  20. data/lib/brute/prompts/objectivity.rb +16 -0
  21. data/lib/brute/prompts/plan_reminder.rb +40 -0
  22. data/lib/brute/prompts/proactiveness.rb +19 -0
  23. data/lib/brute/prompts/security_and_safety.rb +17 -0
  24. data/lib/brute/prompts/skills.rb +22 -0
  25. data/lib/brute/prompts/task_management.rb +59 -0
  26. data/lib/brute/prompts/text/agents/compaction.txt +15 -0
  27. data/lib/brute/prompts/text/agents/explore.txt +17 -0
  28. data/lib/brute/prompts/text/agents/summary.txt +11 -0
  29. data/lib/brute/prompts/text/agents/title.txt +40 -0
  30. data/lib/brute/prompts/text/doing_tasks/anthropic.txt +11 -0
  31. data/lib/brute/prompts/text/doing_tasks/default.txt +6 -0
  32. data/lib/brute/prompts/text/doing_tasks/google.txt +9 -0
  33. data/lib/brute/prompts/text/identity/anthropic.txt +5 -0
  34. data/lib/brute/prompts/text/identity/default.txt +3 -0
  35. data/lib/brute/prompts/text/identity/google.txt +1 -0
  36. data/lib/brute/prompts/text/identity/openai.txt +3 -0
  37. data/lib/brute/prompts/text/tone_and_style/anthropic.txt +5 -0
  38. data/lib/brute/prompts/text/tone_and_style/default.txt +9 -0
  39. data/lib/brute/prompts/text/tone_and_style/google.txt +6 -0
  40. data/lib/brute/prompts/text/tone_and_style/openai.txt +17 -0
  41. data/lib/brute/prompts/text/tool_usage/anthropic.txt +16 -0
  42. data/lib/brute/prompts/text/tool_usage/default.txt +4 -0
  43. data/lib/brute/prompts/text/tool_usage/google.txt +4 -0
  44. data/lib/brute/prompts/tone_and_style.rb +11 -0
  45. data/lib/brute/prompts/tool_usage.rb +11 -0
  46. data/lib/brute/providers/models_dev.rb +111 -0
  47. data/lib/brute/providers/opencode_go.rb +38 -0
  48. data/lib/brute/providers/opencode_zen.rb +82 -0
  49. data/lib/brute/providers/shell.rb +108 -0
  50. data/lib/brute/providers/shell_response.rb +100 -0
  51. data/lib/brute/skill.rb +118 -0
  52. data/lib/brute/system_prompt.rb +141 -63
  53. data/lib/brute/tools/delegate.rb +25 -1
  54. data/lib/brute/tools/question.rb +59 -0
  55. data/lib/brute/version.rb +1 -1
  56. data/lib/brute.rb +83 -3
  57. metadata +49 -1
@@ -0,0 +1,17 @@
1
+ You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
2
+
3
+ Your strengths:
4
+ - Rapidly finding files using glob patterns
5
+ - Searching code and text with powerful regex patterns
6
+ - Reading and analyzing file contents
7
+
8
+ Guidelines:
9
+ - Use fs_search for broad file pattern matching and searching file contents with regex
10
+ - Use read when you know the specific file path you need to read
11
+ - Use shell for file operations like listing directory contents
12
+ - Adapt your search approach based on the thoroughness level specified by the caller
13
+ - Return file paths as absolute paths in your final response
14
+ - For clear communication, avoid using emojis
15
+ - Do not create any files, or run shell commands that modify the user's system state in any way
16
+
17
+ Complete the user's search request efficiently and report your findings clearly.
@@ -0,0 +1,11 @@
1
+ Summarize what was done in this conversation. Write like a pull request description.
2
+
3
+ Rules:
4
+ - 2-3 sentences max
5
+ - Describe the changes made, not the process
6
+ - Do not mention running tests, builds, or other validation steps
7
+ - Do not explain what the user asked for
8
+ - Write in first person (I added..., I fixed...)
9
+ - Never ask questions or add new questions
10
+ - If the conversation ends with an unanswered question to the user, preserve that exact question
11
+ - If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
@@ -0,0 +1,40 @@
1
+ You are a title generator. You output ONLY a thread title. Nothing else.
2
+
3
+ <task>
4
+ Generate a brief title that would help the user find this conversation later.
5
+
6
+ Follow all rules in <rules>
7
+ Use the <examples> so you know what a good title looks like.
8
+ Your output must be:
9
+ - A single line
10
+ - <=50 characters
11
+ - No explanations
12
+ </task>
13
+
14
+ <rules>
15
+ - you MUST use the same language as the user message you are summarizing
16
+ - Title must be grammatically correct and read naturally - no word salad
17
+ - Never include tool names in the title (e.g. "read tool", "shell tool", "patch tool")
18
+ - Focus on the main topic or question the user needs to retrieve
19
+ - Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
20
+ - When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
21
+ - Keep exact: technical terms, numbers, filenames, HTTP codes
22
+ - Remove: the, this, my, a, an
23
+ - Never assume tech stack
24
+ - Never use tools
25
+ - NEVER respond to questions, just generate a title for the conversation
26
+ - The title should NEVER include "summarizing" or "generating" when generating a title
27
+ - DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
28
+ - Always output something meaningful, even if the input is minimal.
29
+ - If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
30
+ -> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
31
+ </rules>
32
+
33
+ <examples>
34
+ "debug 500 errors in production" -> Debugging production 500 errors
35
+ "refactor user service" -> Refactoring user service
36
+ "why is app.js failing" -> app.js failure investigation
37
+ "implement rate limiting" -> Rate limiting implementation
38
+ "how do I connect postgres to my API" -> Postgres API connection
39
+ "best practices for React hooks" -> React hooks best practices
40
+ </examples>
@@ -0,0 +1,11 @@
1
+ # Doing tasks
2
+ The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
3
+ - Use the todo_write tool to plan the task if required
4
+ - Always read before editing: Use `read` to examine a file before using `patch` or `write` to modify it.
5
+ - Verify your changes: After editing, re-read the file or run tests to confirm correctness.
6
+ - Use fs_search to find code: Don't guess file locations — search first.
7
+ - Use shell for git, tests, builds: Run `git diff`, `git status`, test suites, etc.
8
+ - Be precise with patch: The `old_string` must match the file content exactly, including whitespace.
9
+ - Prefer patch over write: For existing files, use `patch` to change specific sections rather than rewriting the entire file.
10
+ - Use undo to recover: If a write or patch goes wrong, use `undo` to restore the previous version.
11
+ - Delegate research: Use `delegate` for complex analysis that needs focused investigation.
@@ -0,0 +1,6 @@
1
+ # Doing tasks
2
+ The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
3
+ - Use the available search tools to understand the codebase and the user's query. Use search tools extensively both in parallel and sequentially.
4
+ - Implement the solution using all tools available to you
5
+ - Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
6
+ - NEVER commit changes unless the user explicitly asks you to.
@@ -0,0 +1,9 @@
1
+ # Primary Workflows
2
+
3
+ ## Software Engineering Tasks
4
+ When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
5
+ 1. **Understand:** Use fs_search and read tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions.
6
+ 2. **Plan:** Build a coherent plan based on your understanding. Share a concise plan with the user if it would help them understand your approach.
7
+ 3. **Implement:** Use the available tools (patch, write, shell, etc.) to act on the plan, strictly adhering to the project's established conventions.
8
+ 4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. NEVER assume standard test commands.
9
+ 5. **Verify (Standards):** After making code changes, execute the project-specific build, linting and type-checking commands that you have identified for this project.
@@ -0,0 +1,5 @@
1
+ You are Brute, an expert software engineering agent.
2
+
3
+ You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
4
+
5
+ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
@@ -0,0 +1,3 @@
1
+ You are Brute, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
2
+
3
+ IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
@@ -0,0 +1 @@
1
+ You are Brute, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.
@@ -0,0 +1,3 @@
1
+ You are Brute. You and the user share the same workspace and collaborate to achieve the user's goals.
2
+
3
+ You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.
@@ -0,0 +1,5 @@
1
+ # Tone and style
2
+ - Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
3
+ - Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
4
+ - Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
5
+ - NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
@@ -0,0 +1,9 @@
1
+ # Tone and style
2
+ You should be concise, direct, and to the point. When you run a non-trivial shell command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing.
3
+ Remember that your output will be displayed on a command line interface. Your responses can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
4
+ Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
5
+ If you cannot or will not help the user with something, please do not say why or what it could lead to. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
6
+ Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
7
+ IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand.
8
+ IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
9
+ IMPORTANT: Keep your responses short. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless the user asks for detail.
@@ -0,0 +1,6 @@
1
+ # Tone and Style
2
+ - **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
3
+ - **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use/code generation) per response whenever practical.
4
+ - **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes..."). Get straight to the action or answer.
5
+ - **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
6
+ - **Tools vs. Text:** Use tools for actions, text output *only* for communication.
@@ -0,0 +1,17 @@
1
+ # Working with the user
2
+
3
+ Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements ("Done —", "Got it", "Great question") or framing phrases.
4
+
5
+ Balance conciseness to not overwhelm the user with appropriate detail for the request. Do not narrate abstractly; explain what you are doing and why.
6
+
7
+ ## Formatting rules
8
+
9
+ Your responses are rendered as GitHub-flavored Markdown.
10
+
11
+ Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections.
12
+
13
+ Use inline code blocks for commands, paths, environment variables, function names, inline examples, keywords.
14
+
15
+ Code samples or multi-line snippets should be wrapped in fenced code blocks. Include a language tag when possible.
16
+
17
+ Don't use emojis or em dashes unless explicitly instructed.
@@ -0,0 +1,16 @@
1
+ # Tool usage policy
2
+ - When doing file search, prefer to use the delegate tool in order to reduce context usage.
3
+ - You should proactively use the delegate tool with specialized agents when the task at hand matches the agent's description.
4
+ - You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
5
+ - Use specialized tools instead of shell commands when possible, as this provides a better user experience. For file operations, use dedicated tools: read for reading files instead of cat/head/tail, patch for editing instead of sed/awk, and write for creating files instead of cat with heredoc or echo redirection. Reserve the shell tool exclusively for actual system commands and terminal operations that require shell execution. NEVER use shell echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
6
+ - VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the delegate tool instead of running search commands directly.
7
+ <example>
8
+ user: Where are errors from the client handled?
9
+ assistant: [Uses the delegate tool to find the files that handle client errors instead of using fs_search directly]
10
+ </example>
11
+ <example>
12
+ user: What is the codebase structure?
13
+ assistant: [Uses the delegate tool]
14
+ </example>
15
+
16
+ IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the conversation.
@@ -0,0 +1,4 @@
1
+ # Tool usage policy
2
+ - When doing file search, prefer to use the delegate tool in order to reduce context usage.
3
+ - You can call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance.
4
+ - Use specialized tools (read, patch, write, fs_search) instead of shell commands for file operations.
@@ -0,0 +1,4 @@
1
+ # Tool Usage
2
+ - **Parallelism:** Execute multiple independent tool calls in parallel when feasible.
3
+ - **Specialized tools:** Use read, patch, write, and fs_search instead of shell commands for file operations.
4
+ - **Interactive Commands:** Avoid shell commands that require user interaction (e.g. `git rebase -i`). Use non-interactive versions of commands.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Prompts
5
+ module ToneAndStyle
6
+ def self.call(ctx)
7
+ Prompts.read("tone_and_style", ctx[:provider_name])
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Prompts
5
+ module ToolUsage
6
+ def self.call(ctx)
7
+ Prompts.read("tool_usage", ctx[:provider_name])
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module Brute
7
+ module Providers
8
+ # Fetches and caches model metadata from the models.dev catalog.
9
+ #
10
+ # Quacks like llm.rb's provider.models so that the REPL's model
11
+ # picker can call:
12
+ #
13
+ # provider.models.all.select(&:chat?)
14
+ #
15
+ # Models are fetched from https://models.dev/api.json and cached
16
+ # in-memory for the lifetime of the process (with a TTL).
17
+ #
18
+ class ModelsDev
19
+ CATALOG_URL = "https://models.dev/api.json"
20
+ CACHE_TTL = 3600 # 1 hour
21
+
22
+ ModelEntry = Struct.new(:id, :name, :chat?, :cost, :limit, :reasoning, :tool_call, keyword_init: true)
23
+
24
+ # @param provider [LLM::Provider] the provider instance (for delegating execute/headers)
25
+ # @param provider_id [String] the provider key in models.dev (e.g., "opencode", "opencode-go")
26
+ def initialize(provider:, provider_id: "opencode")
27
+ @provider = provider
28
+ @provider_id = provider_id
29
+ end
30
+
31
+ # Returns all models for this provider from the models.dev catalog.
32
+ # @return [Array<ModelEntry>]
33
+ def all
34
+ entries = fetch_provider_models
35
+ entries.map do |id, model|
36
+ ModelEntry.new(
37
+ id: id,
38
+ name: model["name"] || id,
39
+ chat?: true,
40
+ cost: model["cost"],
41
+ limit: model["limit"],
42
+ reasoning: model["reasoning"] || false,
43
+ tool_call: model["tool_call"] || false
44
+ )
45
+ end.sort_by(&:id)
46
+ end
47
+
48
+ private
49
+
50
+ def fetch_provider_models
51
+ catalog = self.class.fetch_catalog
52
+ provider_data = catalog[@provider_id]
53
+ return {} unless provider_data
54
+
55
+ provider_data["models"] || {}
56
+ end
57
+
58
+ class << self
59
+ # Fetch the models.dev catalog, with in-memory caching.
60
+ # Thread-safe via a simple mutex.
61
+ def fetch_catalog
62
+ @mutex ||= Mutex.new
63
+ @mutex.synchronize do
64
+ if @catalog && @fetched_at && (Time.now - @fetched_at < CACHE_TTL)
65
+ return @catalog
66
+ end
67
+
68
+ @catalog = download_catalog
69
+ @fetched_at = Time.now
70
+ @catalog
71
+ end
72
+ end
73
+
74
+ # Force a cache refresh on next access.
75
+ def invalidate_cache!
76
+ @mutex&.synchronize do
77
+ @catalog = nil
78
+ @fetched_at = nil
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def download_catalog
85
+ uri = URI.parse(CATALOG_URL)
86
+ http = Net::HTTP.new(uri.host, uri.port)
87
+ http.use_ssl = true
88
+ http.open_timeout = 10
89
+ http.read_timeout = 30
90
+
91
+ request = Net::HTTP::Get.new(uri.request_uri)
92
+ request["User-Agent"] = "brute/#{Brute::VERSION}"
93
+ request["Accept"] = "application/json"
94
+
95
+ response = http.request(request)
96
+
97
+ unless response.is_a?(Net::HTTPSuccess)
98
+ raise "Failed to fetch models.dev catalog: HTTP #{response.code}"
99
+ end
100
+
101
+ JSON.parse(response.body)
102
+ rescue => e
103
+ # Return empty catalog on failure so the provider still works
104
+ # with default_model, just without a model list.
105
+ warn "[brute] Warning: Could not fetch models.dev catalog: #{e.message}"
106
+ {}
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ ##
5
+ # OpenAI-compatible provider for the OpenCode Go API gateway.
6
+ #
7
+ # OpenCode Go is the low-cost subscription plan with a restricted
8
+ # (lite) model list. Same gateway as Zen, different endpoint path.
9
+ #
10
+ # @example
11
+ # llm = LLM::OpencodeGo.new(key: ENV["OPENCODE_API_KEY"])
12
+ # ctx = LLM::Context.new(llm)
13
+ # ctx.talk "Hello from brute"
14
+ #
15
+ class OpencodeGo < OpencodeZen
16
+ ##
17
+ # @return [Symbol]
18
+ def name
19
+ :opencode_go
20
+ end
21
+
22
+ ##
23
+ # Returns models from the models.dev catalog.
24
+ # Note: The Go gateway only accepts lite-tier models, but models.dev
25
+ # doesn't distinguish between Zen and Go tiers. We show the full
26
+ # catalog; the gateway returns an error for unsupported models.
27
+ # @return [Brute::Providers::ModelsDev]
28
+ def models
29
+ Brute::Providers::ModelsDev.new(provider: self, provider_id: "opencode")
30
+ end
31
+
32
+ private
33
+
34
+ def completions_path
35
+ "/zen/go/v1/chat/completions"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure the OpenAI provider is loaded (llm.rb lazy-loads providers).
4
+ unless defined?(LLM::OpenAI)
5
+ require "llm/providers/openai"
6
+ end
7
+
8
+ module LLM
9
+ ##
10
+ # OpenAI-compatible provider for the OpenCode Zen API gateway.
11
+ #
12
+ # OpenCode Zen is a curated model gateway at opencode.ai that proxies
13
+ # requests to upstream LLM providers (Anthropic, OpenAI, Google, etc.).
14
+ # All models are accessed via the OpenAI-compatible chat completions
15
+ # endpoint; the gateway handles format conversion internally.
16
+ #
17
+ # @example
18
+ # llm = LLM::OpencodeZen.new(key: ENV["OPENCODE_API_KEY"])
19
+ # ctx = LLM::Context.new(llm)
20
+ # ctx.talk "Hello from brute"
21
+ #
22
+ # @example Anonymous access (free models only)
23
+ # llm = LLM::OpencodeZen.new(key: "public")
24
+ # ctx = LLM::Context.new(llm)
25
+ # ctx.talk "Hello"
26
+ #
27
+ class OpencodeZen < OpenAI
28
+ HOST = "opencode.ai"
29
+
30
+ ##
31
+ # @param key [String] OpenCode API key, or "public" for anonymous access
32
+ # @param (see LLM::Provider#initialize)
33
+ def initialize(key: "public", **)
34
+ super(host: HOST, key: key, **)
35
+ end
36
+
37
+ ##
38
+ # @return [Symbol]
39
+ def name
40
+ :opencode_zen
41
+ end
42
+
43
+ ##
44
+ # Returns the default model (Claude Sonnet 4, the most common Zen model).
45
+ # @return [String]
46
+ def default_model
47
+ "claude-sonnet-4-20250514"
48
+ end
49
+
50
+ ##
51
+ # Returns models from the models.dev catalog for the opencode provider.
52
+ # @return [Brute::Providers::ModelsDev]
53
+ def models
54
+ Brute::Providers::ModelsDev.new(provider: self, provider_id: "opencode")
55
+ end
56
+
57
+ # -- Unsupported sub-APIs --
58
+
59
+ def responses = raise(NotImplementedError, "Use chat completions via the Zen gateway")
60
+ def images = raise(NotImplementedError, "Not supported via Zen gateway")
61
+ def audio = raise(NotImplementedError, "Not supported via Zen gateway")
62
+ def files = raise(NotImplementedError, "Not supported via Zen gateway")
63
+ def moderations = raise(NotImplementedError, "Not supported via Zen gateway")
64
+ def vector_stores = raise(NotImplementedError, "Not supported via Zen gateway")
65
+
66
+ private
67
+
68
+ def completions_path
69
+ "/zen/v1/chat/completions"
70
+ end
71
+
72
+ def headers
73
+ lock do
74
+ (@headers || {}).merge(
75
+ "Content-Type" => "application/json",
76
+ "Authorization" => "Bearer #{@key}",
77
+ "x-opencode-client" => "brute"
78
+ )
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Brute
6
+ module Providers
7
+ # A pseudo-LLM provider that executes user input as code via the
8
+ # existing Brute::Tools::Shell tool.
9
+ #
10
+ # Models correspond to interpreters:
11
+ #
12
+ # bash - pass-through (default)
13
+ # ruby - ruby -e '...'
14
+ # python - python3 -c '...'
15
+ # nix - nix eval --expr '...'
16
+ #
17
+ # The provider's #complete method returns a synthetic response
18
+ # containing a single "shell" tool call. The orchestrator executes
19
+ # it through the normal pipeline — all middleware (message tracking,
20
+ # session persistence, token tracking, etc.) fires as usual.
21
+ #
22
+ class Shell
23
+ MODELS = %w[bash ruby python nix].freeze
24
+
25
+ INTERPRETERS = {
26
+ "bash" => ->(cmd) { cmd },
27
+ "ruby" => ->(cmd) { "ruby -e #{Shellwords.escape(cmd)}" },
28
+ "python" => ->(cmd) { "python3 -c #{Shellwords.escape(cmd)}" },
29
+ "nix" => ->(cmd) { "nix eval --expr #{Shellwords.escape(cmd)}" },
30
+ }.freeze
31
+
32
+ # ── LLM::Provider duck-type interface ──────────────────────────
33
+
34
+ def name = :shell
35
+ def default_model = "bash"
36
+ def user_role = :user
37
+ def tool_role = :tool
38
+ def assistant_role = :assistant
39
+ def system_role = :system
40
+ def tracer = LLM::Tracer::Null.new(self)
41
+
42
+ def complete(prompt, params = {})
43
+ model = params[:model]&.to_s || default_model
44
+ text = extract_text(prompt)
45
+ tools = params[:tools] || []
46
+
47
+ # nil text means we received tool results (second call) —
48
+ # return an empty assistant response so the orchestrator exits.
49
+ return ShellResponse.new(model: model, tools: tools) if text.nil?
50
+
51
+ wrap = INTERPRETERS.fetch(model, INTERPRETERS["bash"])
52
+ command = wrap.call(text)
53
+
54
+ ShellResponse.new(command: command, model: model, tools: tools)
55
+ end
56
+
57
+ # For the REPL model picker: provider.models.all.select(&:chat?)
58
+ def models
59
+ ModelList.new(MODELS)
60
+ end
61
+
62
+ # ── Internals ──────────────────────────────────────────────────
63
+
64
+ private
65
+
66
+ # Extract the user's text from whatever prompt format ctx.talk sends.
67
+ # Returns nil when the prompt contains tool results (the second
68
+ # round-trip) so #complete knows to return an empty response.
69
+ def extract_text(prompt)
70
+ case prompt
71
+ when String
72
+ prompt
73
+ when ::Array
74
+ return nil if prompt.any? { |p| LLM::Function::Return === p }
75
+
76
+ user_msg = prompt.reverse_each.find { |m| m.respond_to?(:role) && m.role.to_s == "user" }
77
+ user_msg&.content.to_s
78
+ else
79
+ if prompt.respond_to?(:to_a)
80
+ msgs = prompt.to_a
81
+ return nil if msgs.any? { |m| m.respond_to?(:content) && LLM::Function::Return === m.content }
82
+
83
+ user_msg = msgs.reverse_each.find { |m| m.respond_to?(:role) && m.role.to_s == "user" }
84
+ user_msg&.content.to_s
85
+ else
86
+ prompt.to_s
87
+ end
88
+ end
89
+ end
90
+
91
+ # ── ModelList ──────────────────────────────────────────────────
92
+
93
+ # Minimal object that quacks like provider.models so the REPL's
94
+ # fetch_models can call provider.models.all.select(&:chat?).
95
+ class ModelList
96
+ ModelEntry = Struct.new(:id, :chat?, keyword_init: true)
97
+
98
+ def initialize(names)
99
+ @entries = names.map { |n| ModelEntry.new(id: n, chat?: true) }
100
+ end
101
+
102
+ def all
103
+ @entries
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Brute
6
+ module Providers
7
+ # Synthetic completion response returned by Brute::Providers::Shell.
8
+ #
9
+ # When +command+ is present, the response contains a single assistant
10
+ # message with a "shell" tool call. The orchestrator picks it up and
11
+ # executes Brute::Tools::Shell through the normal pipeline.
12
+ #
13
+ # When +command+ is nil (tool results round-trip), the response
14
+ # contains an empty assistant message with no tool calls, causing
15
+ # the orchestrator loop to exit.
16
+ #
17
+ class ShellResponse
18
+ def initialize(command: nil, model: "bash", tools: [])
19
+ @command = command
20
+ @model_name = model
21
+ @tools = tools || []
22
+ end
23
+
24
+ def messages
25
+ return [empty_assistant] if @command.nil?
26
+
27
+ call_id = "shell_#{SecureRandom.hex(8)}"
28
+ tool_call = LLM::Object.from(
29
+ id: call_id,
30
+ name: "shell",
31
+ arguments: { "command" => @command },
32
+ )
33
+ original = [{
34
+ "type" => "tool_use",
35
+ "id" => call_id,
36
+ "name" => "shell",
37
+ "input" => { "command" => @command },
38
+ }]
39
+
40
+ [LLM::Message.new(:assistant, "", {
41
+ tool_calls: [tool_call],
42
+ original_tool_calls: original,
43
+ tools: @tools,
44
+ })]
45
+ end
46
+ alias_method :choices, :messages
47
+
48
+ def model
49
+ @model_name
50
+ end
51
+
52
+ def input_tokens
53
+ 0
54
+ end
55
+
56
+ def output_tokens
57
+ 0
58
+ end
59
+
60
+ def reasoning_tokens
61
+ 0
62
+ end
63
+
64
+ def total_tokens
65
+ 0
66
+ end
67
+
68
+ def content
69
+ messages.find(&:assistant?)&.content
70
+ end
71
+
72
+ def content!
73
+ LLM.json.load(content)
74
+ end
75
+
76
+ def reasoning_content
77
+ nil
78
+ end
79
+
80
+ def usage
81
+ LLM::Usage.new(
82
+ input_tokens: 0,
83
+ output_tokens: 0,
84
+ reasoning_tokens: 0,
85
+ total_tokens: 0,
86
+ )
87
+ end
88
+
89
+ # Contract must be included AFTER method definitions —
90
+ # LLM::Contract checks that all required methods exist at include time.
91
+ include LLM::Contract::Completion
92
+
93
+ private
94
+
95
+ def empty_assistant
96
+ LLM::Message.new(:assistant, "")
97
+ end
98
+ end
99
+ end
100
+ end