Module: AgentHarness::Providers::Adapter

Included in:
Base
Defined in:
lib/agent_harness/providers/adapter.rb

Overview

Interface that all providers must implement

This module defines the contract that provider implementations must follow. Include this module in provider classes to ensure they implement the required interface.

Examples:

Implementing a provider

class MyProvider < AgentHarness::Providers::Base
  include AgentHarness::Providers::Adapter

  def self.provider_name
    :my_provider
  end
end

Defined Under Namespace

Modules: ClassMethods

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(base) ⇒ Object



106
107
108
# File 'lib/agent_harness/providers/adapter.rb', line 106

def self.included(base)
  base.extend(ClassMethods)
end

.metadata_package_name(contract, source) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/agent_harness/providers/adapter.rb', line 77

def self.(contract, source)
  return contract[:package_name] if contract[:package_name]
  return source[:package] if source.is_a?(Hash)

  package = contract[:package]
  return package unless package.is_a?(String)

  if package.split("@").first == ""
    package.split("@", 3).first(2).join("@")
  else
    package.split("@", 2).first
  end
end

.normalize_metadata_installation(contract, provider_name:, binary_name:) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/agent_harness/providers/adapter.rb', line 19

def self.(contract, provider_name:, binary_name:)
  return nil unless contract.is_a?(Hash)

  source = contract[:source]
  install_command = contract[:install_command]&.dup

  normalized = {
    provider: provider_name.to_sym,
    source_type: (contract[:source_type] || source),
    package_name: (contract, source),
    default_version: contract[:default_version] || contract[:version] || contract[:resolved_version],
    resolved_version: contract[:resolved_version] || contract[:version] || contract[:default_version],
    supported_version_requirement: (
      contract[:supported_version_requirement] || contract[:version_requirement]
    ),
    binary_name: contract[:binary_name] || binary_name,
    install_command: install_command,
    install_command_string: contract[:install_command_string] || install_command&.join(" ")
  }

  normalized[:requires_postinstall] = contract[:requires_postinstall] if contract.key?(:requires_postinstall)
  normalized[:postinstall_command] = contract[:postinstall_command] if contract.key?(:postinstall_command)

  if contract.key?(:runtime_requirements)
    normalized[:runtime_requirements] =
      (contract[:runtime_requirements])
  end

  normalized
end

.normalize_metadata_runtime_requirements(requirements) ⇒ Array<Hash>?

Normalize an installation contract's runtime_requirements so the stable provider-metadata path exposes the same runtime prerequisites (e.g. Bun for the :omp provider) as the raw installation contract.

Each entry is duplicated so consumers receive a self-contained hash they can mutate top-level keys on without aliasing the provider-owned contract, and the resulting array is frozen to signal that the normalized metadata is read-only.

Parameters:

  • requirements (Array<Hash>, nil)

Returns:

  • (Array<Hash>, nil)

    normalized requirements, or nil when absent



61
62
63
64
65
66
67
68
69
# File 'lib/agent_harness/providers/adapter.rb', line 61

def self.(requirements)
  return requirements unless requirements.is_a?(Array)

  requirements.map do |requirement|
    next requirement.dup if requirement.is_a?(Hash)

    requirement
  end.freeze
end

.normalize_metadata_source_type(source) ⇒ Object



71
72
73
74
75
# File 'lib/agent_harness/providers/adapter.rb', line 71

def self.(source)
  return source[:type]&.to_sym if source.is_a?(Hash)

  source&.to_sym
end

.normalize_metadata_version_requirement(requirement) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/agent_harness/providers/adapter.rb', line 91

def self.(requirement)
  case requirement
  when nil
    nil
  when Array
    if requirement.all? { |entry| entry.is_a?(Array) && entry.length == 2 }
      requirement.map { |operator, version| "#{operator} #{version}" }.join(", ")
    else
      requirement.join(", ")
    end
  else
    requirement.to_s
  end
end

Instance Method Details

#auth_lock_configHash?

Auth lock configuration for providers that need file-based lock serialization (e.g. OAuth refresh token coordination).

Returns:

  • (Hash, nil)

    lock config with :path and :timeout keys, or nil



1285
1286
1287
# File 'lib/agent_harness/providers/adapter.rb', line 1285

def auth_lock_config
  nil
end

#auth_typeSymbol

Authentication type for this provider

Returns:

  • (Symbol)

    :oauth for token-based auth that can expire, :api_key for static API key auth



963
964
965
# File 'lib/agent_harness/providers/adapter.rb', line 963

def auth_type
  :api_key
end

#build_mcp_flags(mcp_servers, working_dir: nil) ⇒ Array<String>

Build provider-specific MCP flags/arguments for CLI invocation

Parameters:

  • mcp_servers (Array<McpServer>)

    MCP server definitions

  • working_dir (String, nil) (defaults to: nil)

    working directory for temp files

Returns:

  • (Array<String>)

    CLI flags to append to the command



996
997
998
# File 'lib/agent_harness/providers/adapter.rb', line 996

def build_mcp_flags(mcp_servers, working_dir: nil)
  []
end

#capabilitiesHash

Provider capabilities

Returns:

  • (Hash)

    capability flags



894
895
896
897
898
899
900
901
902
903
904
# File 'lib/agent_harness/providers/adapter.rb', line 894

def capabilities
  {
    streaming: false,
    file_upload: false,
    vision: false,
    tool_use: false,
    json_mode: false,
    mcp: false,
    dangerous_mode: false
  }
end

#chat_transportObject?

Returns the transport instance used for chat mode.

Providers that support chat override this to return an appropriate transport (e.g. OpenAICompatibleTransport or TextTransport).

Returns:

  • (Object, nil)

    transport instance or nil if unsupported



1079
1080
1081
# File 'lib/agent_harness/providers/adapter.rb', line 1079

def chat_transport
  nil
end

#chat_transport_typeSymbol?

Returns the symbolic transport type for chat without instantiating the transport object. This avoids triggering API key resolution or other authentication side effects during metadata collection.

Returns:

  • (Symbol, nil)

    :openai_compatible, :anthropic, or nil



1088
1089
1090
# File 'lib/agent_harness/providers/adapter.rb', line 1088

def chat_transport_type
  nil
end

#config_file_content(options = {}) ⇒ String?

Generate provider-specific config file content.

Providers that require a config file written before CLI execution (e.g. Codex TOML, Kilocode JSON) should override this method.

Parameters:

  • options (Hash) (defaults to: {})

    provider-specific options for config generation

Returns:

  • (String, nil)

    config file content, or nil when no config is needed



1267
1268
1269
# File 'lib/agent_harness/providers/adapter.rb', line 1267

def config_file_content(options = {})
  nil
end

#configuration_schemaHash

Provider configuration schema for app-driven setup UIs

Returns metadata describing the configurable fields, supported authentication modes, and backend compatibility for this provider. Applications use this to build generic provider-entry forms without hardcoding provider-specific knowledge.

Returns:

  • (Hash)

    with :fields, :auth_modes, :openai_compatible keys



883
884
885
886
887
888
889
# File 'lib/agent_harness/providers/adapter.rb', line 883

def configuration_schema
  {
    fields: [],
    auth_modes: [auth_type],
    openai_compatible: false
  }
end

#dangerous_mode_flagsArray<String>

Get dangerous mode flags

Returns:

  • (Array<String>)

    CLI flags for dangerous mode



1102
1103
1104
# File 'lib/agent_harness/providers/adapter.rb', line 1102

def dangerous_mode_flags
  []
end

#error_classification_patternsHash<Symbol, Array<Regexp>>

Error classification patterns for downstream consumers

Returns patterns grouped by classification category. These patterns encode provider-specific knowledge about how each CLI reports errors and are intended for use by callers outside agent-harness.

Returns:

  • (Hash<Symbol, Array<Regexp>>)

    patterns by category (:auth_expired, :abort, :authentication, :quota)



921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
# File 'lib/agent_harness/providers/adapter.rb', line 921

def error_classification_patterns
  {
    auth_expired: [],
    abort: [],
    authentication: [],
    quota: [
      /requires more credits/i,
      /insufficient credits/i,
      /insufficient balance/i,
      /credit.*exceeded/i,
      /spend limit.*reached/i,
      /billing.*limit/i,
      /(?:weekly|monthly)(?:\/(?:weekly|monthly))?\s+limit\s+exhausted/i
    ]
  }
end

#error_patternsHash<Symbol, Array<Regexp>>

Error patterns for classification

Returns:

  • (Hash<Symbol, Array<Regexp>>)

    error patterns by category



909
910
911
# File 'lib/agent_harness/providers/adapter.rb', line 909

def error_patterns
  {}
end

#execution_semanticsHash

Execution semantics for this provider

Returns a hash describing provider-specific execution behavior so downstream apps do not need to hardcode CLI quirks. This metadata can be used to select the right flags and interpret output.

Returns:

  • (Hash)

    execution semantics



1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
# File 'lib/agent_harness/providers/adapter.rb', line 1235

def execution_semantics
  {
    prompt_delivery: :arg,       # :arg, :stdin, or :flag
    output_format: :text,        # :text or :json
    sandbox_aware: false,        # adjusts behavior inside containers
    uses_subcommand: false,      # e.g. "codex exec", "opencode run"
    non_interactive_flag: nil,   # flag to suppress interactive prompts
    legitimate_exit_codes: [0],  # exit codes that are NOT errors
    stderr_is_diagnostic: true,  # stderr may contain non-error output
    parses_rate_limit_reset: false # can extract Retry-After from output
  }
end

#fetch_mcp_serversArray<Hash>

Fetch configured MCP servers

Returns:

  • (Array<Hash>)

    MCP server configurations



977
978
979
# File 'lib/agent_harness/providers/adapter.rb', line 977

def fetch_mcp_servers
  []
end

#health_statusHash

Health check

Returns:

  • (Hash)

    with :healthy, :message keys



1149
1150
1151
# File 'lib/agent_harness/providers/adapter.rb', line 1149

def health_status
  {healthy: true, message: "OK"}
end

#heartbeat_integration(heartbeat_file_path:) ⇒ Hash

Return structured heartbeat integration wiring for this provider.

Providers that support activity heartbeat return a Hash describing how to wire heartbeat file touches into the CLI execution. The returned contract includes environment variables, execution preparation (config/plugin file writes), and the heartbeat granularity so downstream callers can enable heartbeat-backed idle timeout without hardcoding provider-specific config logic.

Parameters:

  • heartbeat_file_path (String)

    absolute path to the heartbeat file that should be touched on activity

Returns:

  • (Hash)

    heartbeat integration contract:

    • :supported [Boolean] whether heartbeat is available
    • :env [Hash] environment variables to set (empty when unsupported)
    • :preparation [ExecutionPreparation, nil] file writes for config/plugin wiring
    • :granularity [Symbol, nil] heartbeat event granularity (+:tool_call+, :turn, or :progress)


1318
1319
1320
# File 'lib/agent_harness/providers/adapter.rb', line 1318

def heartbeat_integration(heartbeat_file_path:)
  {supported: false, env: {}, preparation: nil, granularity: nil}
end

#noisy_error_patternsArray<Regexp>

Patterns matching noisy/non-actionable error output

Downstream consumers can use these to filter out log noise from provider stderr/stdout that is not meaningful for users.

Returns:

  • (Array<Regexp>)

    noisy error patterns



944
945
946
# File 'lib/agent_harness/providers/adapter.rb', line 944

def noisy_error_patterns
  []
end

#notify_hook_contentString?

Generate provider-specific notification hook content.

Providers that support notification hooks appended to their config file should override this method.

Returns:

  • (String, nil)

    notify hook content, or nil when not applicable



1277
1278
1279
# File 'lib/agent_harness/providers/adapter.rb', line 1277

def notify_hook_content
  nil
end

#parse_rate_limit_reset(output) ⇒ Time?

Parse a rate-limit reset time from provider output

Providers that include rate-limit reset information in their error output can override this to extract it, so the orchestration layer can schedule retries accurately.

Parameters:

  • output (String)

    combined stdout+stderr from the CLI

Returns:

  • (Time, nil)

    when the rate limit resets, or nil if unknown



1256
1257
1258
# File 'lib/agent_harness/providers/adapter.rb', line 1256

def parse_rate_limit_reset(output)
  nil
end

#plan_execution(prompt:, **options) ⇒ Hash

Return the provider CLI execution plan without executing the command.

Parameters:

  • prompt (String)

    the prompt to send

  • options (Hash)

    provider-specific options

Returns:

  • (Hash)

    with :command, :env, and :preparation keys

Raises:

  • (NotImplementedError)


871
872
873
# File 'lib/agent_harness/providers/adapter.rb', line 871

def plan_execution(prompt:, **options)
  raise NotImplementedError, "#{self.class} must implement #plan_execution"
end

#preflight_check(env:, timeout: 10) ⇒ Hash

Lightweight provider-owned preflight check executed before smoke tests.

Parameters:

  • env (Hash)

    request-scoped environment overrides

  • timeout (Numeric) (defaults to: 10)

    time budget in seconds

Returns:

  • (Hash)

    with :healthy and optional :reason keys



1158
1159
1160
# File 'lib/agent_harness/providers/adapter.rb', line 1158

def preflight_check(env:, timeout: 10)
  {healthy: true}
end

#send_message(prompt:, **options) ⇒ Response

Send a message/prompt to the provider

Parameters:

  • prompt (String)

    the prompt to send

  • options (Hash)

    provider-specific options

Options Hash (**options):

  • :model (String)

    model to use

  • :timeout (Integer)

    timeout in seconds

  • :session (String)

    session identifier

  • :dangerous_mode (Boolean)

    skip permission checks

  • :tools (Symbol, Array<String>, nil)

    tool access control. Pass :none to disable all tool access (pure text-in/text-out mode). Pass an Array of tool name strings to selectively disable specific tools via the provider's disallowed-tools mechanism. Defaults to nil (tools enabled, provider default behavior). Providers that do not support tool control will emit a warning and ignore this option — it is never a hard failure.

  • :provider_runtime (ProviderRuntime, Hash, nil)

    per-request runtime overrides (model, base_url, api_provider, env, flags, metadata). For providers that delegate to Providers::Base#send_message, a plain Hash is automatically coerced into a ProviderRuntime. Providers that override #send_message directly are responsible for handling this option.

  • :smoke_test (Boolean)

    when true, signals that this invocation is a lightweight connectivity/health check issued by #smoke_test. Providers may use this flag to adjust command-line arguments (e.g. Kilocode appends --auto --print-logs) or skip interactive features that would cause the process to hang.

Returns:

  • (Response)

    response object with output and metadata

Raises:

  • (NotImplementedError)


862
863
864
# File 'lib/agent_harness/providers/adapter.rb', line 862

def send_message(prompt:, **options)
  raise NotImplementedError, "#{self.class} must implement #send_message"
end

#session_flags(session_id) ⇒ Array<String>

Get session flags for continuation

Parameters:

  • session_id (String)

    the session ID

Returns:

  • (Array<String>)

    CLI flags for session continuation



1117
1118
1119
# File 'lib/agent_harness/providers/adapter.rb', line 1117

def session_flags(session_id)
  []
end

#smoke_test(timeout: nil, provider_runtime: nil) ⇒ Hash

Execute a minimal provider-owned smoke test via the configured executor.

Parameters:

  • timeout (Integer, nil) (defaults to: nil)

    timeout override in seconds

  • provider_runtime (ProviderRuntime, Hash, nil) (defaults to: nil)

    runtime overrides

Returns:

  • (Hash)

    normalized smoke-test result



1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
# File 'lib/agent_harness/providers/adapter.rb', line 1174

def smoke_test(timeout: nil, provider_runtime: nil)
  contract = smoke_test_contract
  raise NotImplementedError, "#{self.class} does not implement #smoke_test_contract" unless contract

  prompt = contract[:prompt]
  if !prompt.is_a?(String) || prompt.strip.empty?
    raise ConfigurationError, "#{self.class}.smoke_test_contract must define a non-empty :prompt"
  end

  response = send_message(
    prompt: prompt,
    timeout: timeout || contract[:timeout],
    provider_runtime: provider_runtime,
    smoke_test: true
  )

  output = response.output.to_s.strip
  expected_output = contract[:expected_output]&.strip
  success = response.success? && (!contract.fetch(:require_output, true) || !output.empty?)
  success &&= expected_output.nil? || output == expected_output

  if success
    return {
      ok: true,
      status: "ok",
      message: contract[:success_message] || "Smoke test passed",
      error_category: nil,
      output: output,
      exit_code: response.exit_code
    }
  end

  message = response.error.to_s.strip
  message = output if message.empty?
  message = "Smoke test failed with exit code #{response.exit_code}" if message.empty?

  {
    ok: false,
    status: "error",
    message: message,
    error_category: classify_smoke_test_message(message),
    output: output,
    exit_code: response.exit_code
  }
rescue TimeoutError => e
  failure_smoke_test_result(e.message, :timeout)
rescue AuthenticationError => e
  failure_smoke_test_result(e.message, :auth_expired)
rescue RateLimitError => e
  failure_smoke_test_result(e.message, e.error_category || :rate_limited)
rescue ProviderError => e
  failure_smoke_test_result(e.message, classify_smoke_test_message(e.message))
end

#smoke_test_contractHash?

Canonical smoke-test contract for this provider instance.

Returns:

  • (Hash, nil)

    smoke-test metadata



1165
1166
1167
# File 'lib/agent_harness/providers/adapter.rb', line 1165

def smoke_test_contract
  self.class.smoke_test_contract if self.class.respond_to?(:smoke_test_contract)
end

#supported_mcp_transportsArray<String>

Supported MCP transport types for this provider

Defaults to ["stdio"]. Providers that support HTTP/SSE transports should override this to include those transports.

Returns:

  • (Array<String>)

    supported transports (e.g. ["stdio", "http"])



987
988
989
# File 'lib/agent_harness/providers/adapter.rb', line 987

def supported_mcp_transports
  %w[stdio]
end

#supports_activity_heartbeat?Boolean

Whether this provider supports activity heartbeat signaling.

Providers that can emit a container-visible liveness signal during long-running CLI execution return true. Downstream callers use this to decide whether heartbeat-backed idle timeout is viable without maintaining provider-name allowlists.

Returns:

  • (Boolean)

    true if the provider can emit heartbeat signals



1297
1298
1299
# File 'lib/agent_harness/providers/adapter.rb', line 1297

def supports_activity_heartbeat?
  false
end

#supports_chat?Boolean

Check if provider supports multi-turn chat mode.

Providers that return true can accept conversation history and return streaming multi-turn responses via send_chat_message.

Returns:

  • (Boolean)

    true if the provider supports chat



1069
1070
1071
# File 'lib/agent_harness/providers/adapter.rb', line 1069

def supports_chat?
  false
end

#supports_dangerous_mode?Boolean

Check if provider supports dangerous mode

Returns:

  • (Boolean)

    true if dangerous mode is supported



1095
1096
1097
# File 'lib/agent_harness/providers/adapter.rb', line 1095

def supports_dangerous_mode?
  capabilities[:dangerous_mode]
end

#supports_mcp?Boolean

Check if provider supports MCP

Returns:

  • (Boolean)

    true if MCP is supported



970
971
972
# File 'lib/agent_harness/providers/adapter.rb', line 970

def supports_mcp?
  capabilities[:mcp]
end

#supports_message_tool_injection?Boolean

Check if provider message mode can inject available tool definitions through the tools: option. Providers that use tools: strictly for disallow lists should leave this as false.

Returns:

  • (Boolean)

    true if skill tools can be merged into message mode



1048
1049
1050
# File 'lib/agent_harness/providers/adapter.rb', line 1048

def supports_message_tool_injection?
  false
end

#supports_sessions?Boolean

Check if provider supports session continuation

Returns:

  • (Boolean)

    true if sessions are supported



1109
1110
1111
# File 'lib/agent_harness/providers/adapter.rb', line 1109

def supports_sessions?
  false
end

#supports_text_mode?Boolean

Check if provider supports text-only mode via direct HTTP transport.

Providers that return true will route mode: :text requests through their REST API instead of the CLI. Providers that return false fall back to the CLI path with tools forcibly disabled.

Returns:

  • (Boolean)

    true if the provider has an HTTP text transport



1059
1060
1061
# File 'lib/agent_harness/providers/adapter.rb', line 1059

def supports_text_mode?
  false
end

#supports_token_counting?Boolean

Whether this provider can extract token usage from CLI output

Returns:

  • (Boolean)

    true if the provider returns token counts



1135
1136
1137
# File 'lib/agent_harness/providers/adapter.rb', line 1135

def supports_token_counting?
  false
end

#supports_tool_control?Boolean

Check if provider supports tool access control (disabling tools)

Returns:

  • (Boolean)

    true if the provider supports the tools: option



1039
1040
1041
# File 'lib/agent_harness/providers/adapter.rb', line 1039

def supports_tool_control?
  false
end

#token_usage_from_api_response(body) ⇒ Hash

Extract token usage from an API response body

Parses the provider-specific API response shape and returns normalized token counts.

Parameters:

  • body (Hash)

    the parsed JSON response body from the provider API

Returns:

  • (Hash)

    with :input_tokens and :output_tokens keys, or empty hash



1128
1129
1130
# File 'lib/agent_harness/providers/adapter.rb', line 1128

def token_usage_from_api_response(body)
  {}
end

#translate_error(message) ⇒ String

Translate a raw error message into a user-friendly string

Providers override this to map CLI-specific error output into concise, actionable messages.

Parameters:

  • message (String)

    raw error message

Returns:

  • (String)

    translated message (or the original if no match)



955
956
957
# File 'lib/agent_harness/providers/adapter.rb', line 955

def translate_error(message)
  message
end

#validate_configHash

Validate provider configuration

Returns:

  • (Hash)

    with :valid, :errors keys



1142
1143
1144
# File 'lib/agent_harness/providers/adapter.rb', line 1142

def validate_config
  {valid: true, errors: []}
end

#validate_mcp_servers!(mcp_servers) ⇒ Object

Validate that this provider can handle the given MCP servers

Parameters:

  • mcp_servers (Array<McpServer>)

    MCP server definitions

Raises:



1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
# File 'lib/agent_harness/providers/adapter.rb', line 1005

def validate_mcp_servers!(mcp_servers)
  return if mcp_servers.nil? || mcp_servers.empty?

  unless supports_mcp?
    raise McpUnsupportedError.new(
      "Provider '#{self.class.provider_name}' does not support MCP servers",
      provider: self.class.provider_name
    )
  end

  supported = supported_mcp_transports

  if supported.empty?
    raise McpUnsupportedError.new(
      "Provider '#{self.class.provider_name}' does not support request-time MCP servers",
      provider: self.class.provider_name
    )
  end

  mcp_servers.each do |server|
    next if supported.include?(server.transport)

    raise McpTransportUnsupportedError.new(
      "Provider '#{self.class.provider_name}' does not support MCP transport " \
      "'#{server.transport}' (server: '#{server.name}'). " \
      "Supported transports: #{supported.join(", ")}",
      provider: self.class.provider_name
    )
  end
end