Class: Space::Architect::Harness::PiHarness

Inherits:
Object
  • Object
show all
Defined in:
lib/space_architect/harness.rb

Overview

pi -p --mode json, session redirected into the lane's build dir via --session-dir (pi otherwise auto-saves under ~/.pi/agent/sessions/, organized by mangled CWD).

Constant Summary collapse

TIMEOUT_EXIT_CODE =
124

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model:, max_turns:, bin: nil, config_dir:, effort: nil) ⇒ PiHarness

max_turns is accepted for interface parity with the other harnesses but is intentionally absent from argv — pi has no turn cap; the wall-clock timeout (below) is the only bound on a run.



493
494
495
496
497
498
# File 'lib/space_architect/harness.rb', line 493

def initialize(model:, max_turns:, bin: nil, config_dir:, effort: nil)
  @model      = model
  @bin        = bin || ENV.fetch("ARCHITECT_PI_BIN", "pi")
  @config_dir = Pathname.new(config_dir)
  @effort     = effort
end

Class Method Details

.translate_thinking(level, force: false) ⇒ Object

pi's --thinking accepts the full normalized vocabulary unchanged — pi's own per-model thinkingLevelMap clamps further; architect does not clamp for pi.



484
485
486
487
488
# File 'lib/space_architect/harness.rb', line 484

def self.translate_thinking(level, force: false)
  return [nil, nil] if level.nil?
  return [level, "thinking: force --effort=#{level} (unmodified, may be rejected)"] if force
  [level, nil]
end

Instance Method Details

#run(prompt_path:, run_log_path:, chdir:, timeout: nil) ⇒ Object



500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
# File 'lib/space_architect/harness.rb', line 500

def run(prompt_path:, run_log_path:, chdir:, timeout: nil, **)
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)

  File.open(prompt_path, "r") do |prompt_io|
    File.open(run_log_path, "w") do |log|
      Sync do
        child = Async::Process::Child.new(*argv, chdir: chdir.to_s, in: prompt_io, out: log, err: log)
        timed_out    = false
        timeout_task = nil

        # Same TERM→grace→KILL escalation as ClaudeCodeHarness — with: Async::Process::Child's
        # #wait_thread ensure goes straight to KILL, so a concurrent fiber does the escalation.
        if timeout && timeout > 0
          timeout_task = Async(transient: true) do
            sleep timeout
            timed_out = true
            Process.kill("TERM", -child.pid) rescue nil
            sleep 0.5
            Process.kill("KILL", -child.pid) rescue nil
          end
        end

        status = child.wait
        timeout_task&.stop

        timed_out ? TIMEOUT_EXIT_CODE : status.exitstatus
      end
    end
  end
end

#run_detached(prompt_path:, run_log_path:, chdir:) ⇒ Object



532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/space_architect/harness.rb', line 532

def run_detached(prompt_path:, run_log_path:, chdir:)
  prompt_path  = Pathname.new(prompt_path)
  run_log_path = Pathname.new(run_log_path)

  prompt_io = File.open(prompt_path, "r")
  log       = File.open(run_log_path, "w")
  begin
    pid = Process.spawn(*argv, chdir: chdir.to_s, pgroup: true,
                        in: prompt_io, out: log, err: log)
    Process.detach(pid)
  ensure
    prompt_io.close
    log.close
  end
  pid
end