Class: Harnex::Adapters::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/harnex/adapters/base.rb

Direct Known Subclasses

Claude, Codex, CodexAppServer, Generic, Opencode, Pi

Constant Summary collapse

PROMPT_PREFIXES =
[">", "\u203A", "\u276F"].freeze
AGENT_VERSION_TIMEOUT_SECONDS =
2.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, extra_args = []) ⇒ Base

Returns a new instance of Base.



22
23
24
25
# File 'lib/harnex/adapters/base.rb', line 22

def initialize(key, extra_args = [])
  @key = key
  @extra_args = extra_args.dup
end

Instance Attribute Details

#keyObject (readonly)

Adapter contract — subclasses MUST implement:

base_command          -> Array[String]  CLI args to spawn

Subclasses MAY override:

input_state(text)     -> Hash           Parse screen for state
build_send_payload    -> Hash           Build injection payload
inject_exit(writer)   -> void           Send a stop/exit sequence
infer_repo_path(argv) -> String         Extract repo path from CLI args
wait_for_sendable     -> String         Wait for a send-ready snapshot


20
21
22
# File 'lib/harnex/adapters/base.rb', line 20

def key
  @key
end

Instance Method Details

#agent_versionObject

Probes ‘<base_command.first> –version` with a short timeout and memoizes the result for the adapter’s lifetime. Returns nil when the binary is missing, exits non-zero, or stalls past the timeout.



43
44
45
46
47
# File 'lib/harnex/adapters/base.rb', line 43

def agent_version
  return @agent_version if defined?(@agent_version)

  @agent_version = probe_agent_version
end

#base_commandObject

Raises:

  • (NotImplementedError)


57
58
59
# File 'lib/harnex/adapters/base.rb', line 57

def base_command
  raise NotImplementedError, "#{self.class} must define #base_command"
end

#build_commandObject



53
54
55
# File 'lib/harnex/adapters/base.rb', line 53

def build_command
  base_command + @extra_args
end

#build_send_payload(text:, submit:, enter_only:, screen_text:, force: false) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/harnex/adapters/base.rb', line 104

def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false)
  state = input_state(screen_text)
  if !force && blocked_state?(state, enter_only: enter_only)
    raise ArgumentError, blocked_message(state, enter_only: enter_only)
  end

  payload = enter_only ? "" : text.to_s
  payload << submit_bytes if submit || enter_only

  {
    text: payload,
    newline: false,
    input_state: state,
    force: force
  }
end

#describeObject



49
50
51
# File 'lib/harnex/adapters/base.rb', line 49

def describe
  { transport: transport }
end

#infer_repo_path(_argv) ⇒ Object



61
62
63
# File 'lib/harnex/adapters/base.rb', line 61

def infer_repo_path(_argv)
  Dir.pwd
end

#inject_exit(writer, delay_ms: 0) ⇒ Object



121
122
123
124
125
126
127
# File 'lib/harnex/adapters/base.rb', line 121

def inject_exit(writer, delay_ms: 0)
  writer.write("/exit")
  writer.flush
  sleep(delay_ms / 1000.0) if delay_ms.positive?
  writer.write(submit_bytes)
  writer.flush
end

#input_state(screen_text) ⇒ Object



65
66
67
68
69
70
# File 'lib/harnex/adapters/base.rb', line 65

def input_state(screen_text)
  {
    state: "unknown",
    input_ready: nil
  }
end

#parse_session_summary(_transcript_tail) ⇒ Object



72
73
74
# File 'lib/harnex/adapters/base.rb', line 72

def parse_session_summary(_transcript_tail)
  {}
end

#providerObject

Vendor of the underlying agent — populates DISPATCH meta.agent_provider. Subclasses override (claude → “anthropic”, codex → “openai”).



36
37
38
# File 'lib/harnex/adapters/base.rb', line 36

def provider
  nil
end

#send_wait_seconds(submit:, enter_only:) ⇒ Object



76
77
78
# File 'lib/harnex/adapters/base.rb', line 76

def send_wait_seconds(submit:, enter_only:)
  0.0
end

#transportObject

Default transport. Structured adapters override to :stdio_jsonrpc (Codex app-server) or :stdio_jsonl_rpc (Pi RPC); Session#run uses this to pick the I/O path.



30
31
32
# File 'lib/harnex/adapters/base.rb', line 30

def transport
  :pty
end

#wait_for_sendable(screen_snapshot_fn, submit:, enter_only:, force:) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/harnex/adapters/base.rb', line 84

def wait_for_sendable(screen_snapshot_fn, submit:, enter_only:, force:)
  snapshot = screen_snapshot_fn.call
  return snapshot if force

  wait_secs = send_wait_seconds(submit: submit, enter_only: enter_only).to_f
  return snapshot unless wait_secs.positive?

  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait_secs
  state = input_state(snapshot)

  while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline &&
        wait_for_sendable_state?(state, submit: submit, enter_only: enter_only)
    sleep 0.05
    snapshot = screen_snapshot_fn.call
    state = input_state(snapshot)
  end

  snapshot
end

#wait_for_sendable_state?(_state, submit:, enter_only:) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/harnex/adapters/base.rb', line 80

def wait_for_sendable_state?(_state, submit:, enter_only:)
  false
end