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.



443
444
445
446
447
448
# File 'lib/space_architect/harness.rb', line 443

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.



434
435
436
437
438
# File 'lib/space_architect/harness.rb', line 434

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



450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'lib/space_architect/harness.rb', line 450

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



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/space_architect/harness.rb', line 482

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