Class: Miniswen::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/miniswen/agent.rb

Overview

A Ruby port of mini-swe-agent's loop (mini.yaml at commit a83fcae): ask the model for bash tool calls, run them, repeat until it submits or a limit trips.

Defined Under Namespace

Classes: BashTool, CostSource, Result

Constant Summary collapse

SUBMIT_MARKER =
"COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT"
MAX_OBSERVATION_CHARS =
10_000
MAX_CONSECUTIVE_FORMAT_ERRORS =
3
TRUNCATION_FINISH_REASONS =

Both finish_reason dialects accepted raw: OpenAI-shaped providers say "length"/"tool_calls", Anthropic says "max_tokens"/"tool_use".

%w[length max_tokens].freeze
CLAIMED_TOOL_FINISH_REASONS =
%w[tool_calls tool_use].freeze
EXEC_ENV =
{
  "PAGER" => "cat",
  "MANPAGER" => "cat",
  "LESS" => "-R",
  "PIP_PROGRESS_BAR" => "off",
  "TQDM_DISABLE" => "1"
}.freeze
LOCAL_PROVIDERS =

Providers that serve local inference (and cost zero)

%i[ollama gpustack].freeze
SYSTEM_TEMPLATE =
<<~PROMPT
  You are a helpful assistant that can interact with a computer.
PROMPT
INSTANCE_TEMPLATE =
<<~PROMPT.freeze
  Please solve this issue: %<instruction>s

  You can execute bash commands and edit files to implement the necessary changes.

  ## Recommended Workflow

  This workflow should be done step-by-step so that you can iterate on your changes and any possible problems.

  1. Analyze the codebase by finding and reading relevant files
  2. Create a script to reproduce the issue
  3. Edit the source code to resolve the issue
  4. Verify your fix works by running your script again
  5. Test edge cases to ensure your fix is robust
  6. Submit your changes and finish your work by issuing the following command: `echo #{SUBMIT_MARKER}`.
     Do not combine it with any other command. <important>After this command, you cannot continue working on this task.</important>

  ## Command Execution Rules

  You are operating in an environment where

  1. You issue at least one command
  2. The system executes the command(s) in a subshell
  3. You see the result(s)
  4. You write your next command(s)

  Each response should include:

  1. **Reasoning text** where you explain your analysis and plan
  2. At least one tool call with your command

  **CRITICAL REQUIREMENTS:**

  - Your response SHOULD include reasoning text explaining what you're doing
  - Your response MUST include AT LEAST ONE bash tool call
  - Directory or environment variable changes are not persistent. Every action is executed in a new subshell.
  - However, you can prefix any action with `MY_ENV_VAR=MY_VALUE cd /path/to/working/dir && ...` or write/load environment variables from files
  - Submit your changes and finish your work by issuing the following command: `echo #{SUBMIT_MARKER}`.
    Do not combine it with any other command. <important>After this command, you cannot continue working on this task.</important>

  Example of a CORRECT response:
  <example_response>
  I need to understand the structure of the repository first. Let me check what files are in the current directory to get a better understanding of the codebase.

  [Makes bash tool call with {"command": "ls -la"} as arguments]
  </example_response>

  <system_information>
  %<system_information>s
  </system_information>

  ## Useful command examples

  ### Create a new file:

  ```bash
  cat <<'EOF' > newfile.py
  import numpy as np
  hello = "ciao"
  print(hello)
  EOF
  ```

  ### Edit files with sed:
  %<macos_sed_note>s
  ```bash
  # Replace all occurrences
  sed -i 's/old_string/new_string/g' filename.py

  # Replace only first occurrence
  sed -i 's/old_string/new_string/' filename.py

  # Replace first occurrence on line 1
  sed -i '1s/old_string/new_string/' filename.py

  # Replace all occurrences in lines 1-10
  sed -i '1,10s/old_string/new_string/g' filename.py
  ```

  ### View file content:

  ```bash
  # View specific lines with numbers
  nl -ba filename.py | sed -n '10,20p'
  ```

  ### Any other command you want to run

  ```bash
  anything
  ```
PROMPT
MACOS_SED_NOTE =
<<~NOTE
  <important>
  You are on MacOS. For all the below examples, you need to use `sed -i ''` instead of `sed -i`.
  </important>
NOTE
NO_TOOL_CALLS_ERROR =
"No tool calls found in the response. Every response MUST include at least one tool call."
TRUNCATION_ERROR_MESSAGE =
<<~MESSAGE
  Your previous response reached the output token limit (finish_reason=%<finish_reason>s) before you produced a tool call, so it was cut off. Respond more concisely and finish with exactly one bash tool call. If you need to think more, do so briefly.
MESSAGE
TOOL_CALL_ERROR_MESSAGE =
<<~MESSAGE.freeze
  Tool call error:

  <error>
  %<error>s
  </error>

  Here is general guidance on how to submit correct toolcalls:

  Every response needs to use the 'bash' tool at least once to execute commands.

  Call the bash tool with your command as the argument:
  - Tool: bash
  - Arguments: {"command": "your_command_here"}

  If you want to end the task, please issue the following command: `echo #{SUBMIT_MARKER}`
  without any other command.
MESSAGE

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, environment:, max_steps: 0, max_time: 0, max_cost: nil, exec_timeout: 30, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, reporter: nil) ⇒ Agent

model is a litellm-style name ("openrouter/z-ai/glm-5.2"). Limits of 0 or nil are disabled.



214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/miniswen/agent.rb', line 214

def initialize(model:, environment:, max_steps: 0, max_time: 0, max_cost: nil,
               exec_timeout: 30, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
               reporter: nil)
  @provider, @id = model.split("/", 2)
  unless @id
    @id = @provider
    @provider = nil
  end

  @model = model
  @environment = environment

  @bash_tool = BashTool.new

  @max_steps = max_steps.to_i
  @max_time = max_time.to_f
  @max_cost = max_cost
  @exec_timeout = exec_timeout

  @clock = clock
  @reporter = reporter
end

Instance Attribute Details

#environmentObject (readonly)

Returns the value of attribute environment.



208
209
210
# File 'lib/miniswen/agent.rb', line 208

def environment
  @environment
end

#messagesObject (readonly)

Returns the value of attribute messages.



208
209
210
# File 'lib/miniswen/agent.rb', line 208

def messages
  @messages
end

Instance Method Details

#provider_envObject

The env a remote miniswen needs to drive this model: the resolved provider's required config options, named the way ruby_llm.rb reads them back from ENV on boot (the option upcased).



280
281
282
283
# File 'lib/miniswen/agent.rb', line 280

def provider_env
  _, provider = resolved
  provider.configuration_requirements.to_h { [_1.to_s.upcase, RubyLLM.config.public_send(_1)] }.compact
end

#run(instruction) ⇒ Object



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'lib/miniswen/agent.rb', line 237

def run(instruction)
  uname = execute("uname -srvm").output.to_s.strip
  @messages = [
    { role: "system", content: SYSTEM_TEMPLATE },
    { role: "user", content: format(INSTANCE_TEMPLATE,
                                    instruction: instruction,
                                    system_information: uname,
                                    macos_sed_note: uname.start_with?("Darwin") ? "\n#{MACOS_SED_NOTE}" : "") }
  ]

  @steps = 0
  @cost = 0.0

  @totals = { input_tokens: 0, output_tokens: 0, cached_tokens: 0, thinking_tokens: 0 }

  @cost_known = true
  @consecutive_format_errors = 0
  @started_at = @clock.call

  loop do
    (status = limit_reached) and return finish(status)

    actions = next_actions
    if actions.nil?
      return finish(:format_error) if @consecutive_format_errors >= MAX_CONSECUTIVE_FORMAT_ERRORS

      next
    end

    actions.each do |action|
      reporter&.on_tool_call(action)
      result = execute(action.fetch(:arguments).fetch("command"))
      # The submit command's output is observed too, so the final tool
      # call has a linked result in the trajectory.
      observe(action, result)
      return finish(:submitted, submission: submission_from(result)) if (result)
    end
  end
end