Class: Harnex::Adapters::Base

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

Direct Known Subclasses

Claude, Codex, CodexAppServer, Generic, Opencode

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.



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

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

  @agent_version = probe_agent_version
end

#base_commandObject

Raises:

  • (NotImplementedError)


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

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

#build_commandObject



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

def build_command
  base_command + @extra_args
end

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



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

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



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

def describe
  { transport: transport }
end

#infer_repo_path(_argv) ⇒ Object



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

def infer_repo_path(_argv)
  Dir.pwd
end

#inject_exit(writer, delay_ms: 0) ⇒ Object



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

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



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

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

#parse_session_summary(_transcript_tail) ⇒ Object



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

def parse_session_summary(_transcript_tail)
  {}
end

#providerObject

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



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

def provider
  nil
end

#send_wait_seconds(submit:, enter_only:) ⇒ Object



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

def send_wait_seconds(submit:, enter_only:)
  0.0
end

#transportObject

Default transport. Adapters speaking JSON-RPC override to :stdio_jsonrpc; Session#run uses this to pick the I/O path.



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

def transport
  :pty
end

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



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

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)


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

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