agent-harness 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +23 -0
- data/README.md +36 -5
- data/lib/agent_harness/authentication.rb +47 -7
- data/lib/agent_harness/conversation.rb +326 -0
- data/lib/agent_harness/errors.rb +3 -0
- data/lib/agent_harness/mcp_server.rb +32 -0
- data/lib/agent_harness/openai_compatible_transport.rb +391 -0
- data/lib/agent_harness/provider_runtime.rb +40 -4
- data/lib/agent_harness/providers/adapter.rb +62 -3
- data/lib/agent_harness/providers/anthropic.rb +30 -0
- data/lib/agent_harness/providers/base.rb +142 -0
- data/lib/agent_harness/providers/codex.rb +26 -3
- data/lib/agent_harness/providers/github_copilot.rb +130 -74
- data/lib/agent_harness/text_transport.rb +320 -13
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +28 -2
- metadata +3 -1
|
@@ -25,13 +25,61 @@ module AgentHarness
|
|
|
25
25
|
DEFAULT_MAX_TOKENS = 4096
|
|
26
26
|
DEFAULT_TIMEOUT = 300
|
|
27
27
|
|
|
28
|
+
# @param base_url [String] Anthropic Messages API URL
|
|
28
29
|
# @param api_key [String] Anthropic API key
|
|
29
30
|
# @param logger [Logger, nil] optional logger
|
|
30
|
-
def initialize(api_key:, logger: nil)
|
|
31
|
+
def initialize(api_key:, base_url: ANTHROPIC_API_URL, logger: nil)
|
|
32
|
+
@base_url = base_url
|
|
31
33
|
@api_key = api_key
|
|
32
34
|
@logger = logger
|
|
33
35
|
end
|
|
34
36
|
|
|
37
|
+
# Send a multi-turn chat completion request via the Anthropic Messages API.
|
|
38
|
+
#
|
|
39
|
+
# @param messages [Array<Hash>] conversation messages with :role and :content
|
|
40
|
+
# @param tools [Array<Hash>, nil] tool definitions (Anthropic tool format)
|
|
41
|
+
# @param stream [Boolean] whether to stream the response
|
|
42
|
+
# @param max_tokens [Integer, nil] maximum tokens in the response
|
|
43
|
+
# @param temperature [Float, nil] sampling temperature
|
|
44
|
+
# @yield [Hash] streaming chunks when stream: true
|
|
45
|
+
# @return [Response] the response
|
|
46
|
+
def chat(messages:, tools: nil, stream: false, max_tokens: nil, temperature: nil,
|
|
47
|
+
model: nil, on_chat_chunk: nil, observer: nil, &on_chunk)
|
|
48
|
+
model ||= DEFAULT_MODEL
|
|
49
|
+
timeout = DEFAULT_TIMEOUT
|
|
50
|
+
max_tokens ||= DEFAULT_MAX_TOKENS
|
|
51
|
+
|
|
52
|
+
uri = URI(@base_url)
|
|
53
|
+
|
|
54
|
+
system_messages = messages.select { |m| m[:role] == "system" || m["role"] == "system" }
|
|
55
|
+
non_system = messages.reject { |m| m[:role] == "system" || m["role"] == "system" }
|
|
56
|
+
has_stream_receiver = on_chunk || on_chat_chunk || observer_responds_to?(observer, :on_chat_chunk)
|
|
57
|
+
request_stream = stream && has_stream_receiver
|
|
58
|
+
|
|
59
|
+
body = build_chat_request_body(
|
|
60
|
+
model: model,
|
|
61
|
+
max_tokens: max_tokens,
|
|
62
|
+
messages: non_system,
|
|
63
|
+
system_messages: system_messages,
|
|
64
|
+
tools: tools,
|
|
65
|
+
temperature: temperature,
|
|
66
|
+
stream: request_stream
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
start_time = Time.now
|
|
70
|
+
|
|
71
|
+
if request_stream
|
|
72
|
+
combined = build_chat_chunk_callback(on_chunk, on_chat_chunk, observer)
|
|
73
|
+
result = make_streaming_request(uri, body, timeout: timeout, &combined)
|
|
74
|
+
duration = Time.now - start_time
|
|
75
|
+
build_streaming_response(result, duration: duration, model: model)
|
|
76
|
+
else
|
|
77
|
+
http_response = make_request(uri, body, timeout: timeout)
|
|
78
|
+
duration = Time.now - start_time
|
|
79
|
+
parse_response(http_response, duration: duration, model: model)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
35
83
|
# Send a text-only message via the Anthropic Messages API.
|
|
36
84
|
#
|
|
37
85
|
# @param prompt [String] the user prompt
|
|
@@ -48,7 +96,7 @@ module AgentHarness
|
|
|
48
96
|
timeout ||= DEFAULT_TIMEOUT
|
|
49
97
|
max_tokens ||= DEFAULT_MAX_TOKENS
|
|
50
98
|
|
|
51
|
-
uri = URI(
|
|
99
|
+
uri = URI(@base_url)
|
|
52
100
|
body = {
|
|
53
101
|
model: model,
|
|
54
102
|
max_tokens: max_tokens,
|
|
@@ -64,25 +112,160 @@ module AgentHarness
|
|
|
64
112
|
|
|
65
113
|
private
|
|
66
114
|
|
|
115
|
+
def build_chat_request_body(model:, max_tokens:, messages:, system_messages:, tools:, temperature:, stream:)
|
|
116
|
+
body = {
|
|
117
|
+
model: model,
|
|
118
|
+
max_tokens: max_tokens,
|
|
119
|
+
messages: messages.map { |m| {role: m[:role] || m["role"], content: m[:content] || m["content"]} }
|
|
120
|
+
}
|
|
121
|
+
body[:system] = system_messages.map { |m| m[:content] || m["content"] }.join("\n") if system_messages.any?
|
|
122
|
+
body[:tools] = tools if tools
|
|
123
|
+
body[:temperature] = temperature if temperature
|
|
124
|
+
body[:stream] = true if stream
|
|
125
|
+
body
|
|
126
|
+
end
|
|
127
|
+
|
|
67
128
|
def make_request(uri, body, timeout:)
|
|
129
|
+
http = build_http(uri, timeout: timeout)
|
|
130
|
+
request = build_post_request(uri, body)
|
|
131
|
+
|
|
132
|
+
@logger&.debug("[AgentHarness::TextTransport] POST #{uri} model=#{body[:model]}")
|
|
133
|
+
|
|
134
|
+
http.request(request)
|
|
135
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
136
|
+
raise TimeoutError.new(e.message, original_error: e)
|
|
137
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, IOError => e
|
|
138
|
+
raise ProviderError.new("HTTP connection error: #{e.message}", original_error: e)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def make_streaming_request(uri, body, timeout:, &on_chunk)
|
|
142
|
+
http = build_http(uri, timeout: timeout)
|
|
143
|
+
request = build_post_request(uri, body)
|
|
144
|
+
|
|
145
|
+
@logger&.debug("[AgentHarness::TextTransport] POST #{uri} model=#{body[:model]} stream=true")
|
|
146
|
+
|
|
147
|
+
accumulated = {content: +"", model: nil, usage: nil, tool_calls: []}
|
|
148
|
+
|
|
149
|
+
http.request(request) do |http_response|
|
|
150
|
+
status_code = http_response.code.to_i
|
|
151
|
+
unless status_code == 200
|
|
152
|
+
response_body = http_response.read_body
|
|
153
|
+
handle_error_response_raw(response_body, status_code)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
parse_sse_stream(http_response, accumulated, &on_chunk)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
accumulated
|
|
160
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
161
|
+
raise TimeoutError.new(e.message, original_error: e)
|
|
162
|
+
rescue SocketError, Errno::ECONNREFUSED, Errno::ECONNRESET, IOError => e
|
|
163
|
+
raise ProviderError.new("HTTP connection error: #{e.message}", original_error: e)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build_http(uri, timeout:)
|
|
68
167
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
69
|
-
http.use_ssl =
|
|
168
|
+
http.use_ssl = (uri.scheme == "https")
|
|
70
169
|
http.open_timeout = [timeout, 30].min
|
|
71
170
|
http.read_timeout = timeout
|
|
171
|
+
http
|
|
172
|
+
end
|
|
72
173
|
|
|
174
|
+
def build_post_request(uri, body)
|
|
73
175
|
request = Net::HTTP::Post.new(uri)
|
|
74
176
|
request["Content-Type"] = "application/json"
|
|
75
177
|
request["x-api-key"] = @api_key
|
|
76
178
|
request["anthropic-version"] = ANTHROPIC_API_VERSION
|
|
77
179
|
request.body = JSON.generate(body)
|
|
180
|
+
request
|
|
181
|
+
end
|
|
78
182
|
|
|
79
|
-
|
|
183
|
+
def parse_sse_stream(http_response, accumulated, &on_chunk)
|
|
184
|
+
buffer = +""
|
|
185
|
+
event_name = nil
|
|
186
|
+
data_lines = []
|
|
80
187
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
188
|
+
http_response.read_body do |chunk|
|
|
189
|
+
buffer << chunk.delete("\r")
|
|
190
|
+
|
|
191
|
+
while (line_end = buffer.index("\n"))
|
|
192
|
+
line = buffer.slice!(0, line_end + 1).chomp("\n")
|
|
193
|
+
|
|
194
|
+
if line.empty?
|
|
195
|
+
process_sse_event(event_name, data_lines.join("\n"), accumulated, &on_chunk)
|
|
196
|
+
event_name = nil
|
|
197
|
+
data_lines = []
|
|
198
|
+
next
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if line.start_with?("event:")
|
|
202
|
+
event_name = line[6..].strip
|
|
203
|
+
elsif line.start_with?("data:")
|
|
204
|
+
data_lines << line[5..].lstrip
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
process_sse_event(event_name, data_lines.join("\n"), accumulated, &on_chunk) unless data_lines.empty?
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def process_sse_event(event_name, raw_data, accumulated, &on_chunk)
|
|
213
|
+
return if raw_data.nil? || raw_data.empty?
|
|
214
|
+
return if event_name == "ping"
|
|
215
|
+
|
|
216
|
+
payload = JSON.parse(raw_data)
|
|
217
|
+
type = payload["type"] || event_name
|
|
218
|
+
|
|
219
|
+
case type
|
|
220
|
+
when "message_start"
|
|
221
|
+
message = payload["message"] || {}
|
|
222
|
+
accumulated[:model] ||= message["model"]
|
|
223
|
+
merge_usage!(accumulated, message["usage"])
|
|
224
|
+
when "content_block_start"
|
|
225
|
+
process_content_block_start(payload, accumulated, &on_chunk)
|
|
226
|
+
when "content_block_delta"
|
|
227
|
+
process_content_block_delta(payload, accumulated, &on_chunk)
|
|
228
|
+
when "content_block_stop"
|
|
229
|
+
process_content_block_stop(payload, accumulated, &on_chunk)
|
|
230
|
+
when "message_delta"
|
|
231
|
+
merge_usage!(accumulated, payload["usage"])
|
|
232
|
+
when "message_stop"
|
|
233
|
+
emit_usage_and_done(accumulated, &on_chunk)
|
|
234
|
+
when "error"
|
|
235
|
+
message = payload.dig("error", "message") || payload.dig("error", "type") || raw_data
|
|
236
|
+
raise ProviderError, message
|
|
237
|
+
end
|
|
238
|
+
rescue JSON::ParserError => e
|
|
239
|
+
@logger&.warn("[AgentHarness::TextTransport] Skipping malformed SSE event: #{e.message}")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def emit_text_delta(text, accumulated, &on_chunk)
|
|
243
|
+
return if text.nil? || text.empty?
|
|
244
|
+
|
|
245
|
+
accumulated[:content] << text
|
|
246
|
+
on_chunk.call({type: :text, content: text})
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def merge_usage!(accumulated, usage)
|
|
250
|
+
return unless usage
|
|
251
|
+
|
|
252
|
+
current = accumulated[:usage] || {input: 0, output: 0, total: 0}
|
|
253
|
+
current[:input] = usage["input_tokens"] unless usage["input_tokens"].nil?
|
|
254
|
+
current[:output] = usage["output_tokens"] unless usage["output_tokens"].nil?
|
|
255
|
+
current[:total] = current[:input].to_i + current[:output].to_i
|
|
256
|
+
accumulated[:usage] = current
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def emit_usage_and_done(accumulated, &on_chunk)
|
|
260
|
+
usage = accumulated[:usage]
|
|
261
|
+
if usage
|
|
262
|
+
on_chunk.call({
|
|
263
|
+
type: :usage,
|
|
264
|
+
input_tokens: usage[:input],
|
|
265
|
+
output_tokens: usage[:output]
|
|
266
|
+
})
|
|
267
|
+
end
|
|
268
|
+
on_chunk.call({type: :done})
|
|
86
269
|
end
|
|
87
270
|
|
|
88
271
|
def parse_response(http_response, duration:, model:)
|
|
@@ -95,6 +278,10 @@ module AgentHarness
|
|
|
95
278
|
body = JSON.parse(http_response.body)
|
|
96
279
|
output = extract_text_content(body)
|
|
97
280
|
tokens = extract_tokens(body)
|
|
281
|
+
tool_calls = extract_tool_calls(body)
|
|
282
|
+
|
|
283
|
+
metadata = {transport: :http}
|
|
284
|
+
metadata[:tool_calls] = tool_calls if tool_calls
|
|
98
285
|
|
|
99
286
|
Response.new(
|
|
100
287
|
output: output,
|
|
@@ -103,7 +290,7 @@ module AgentHarness
|
|
|
103
290
|
provider: :claude,
|
|
104
291
|
model: body["model"] || model,
|
|
105
292
|
tokens: tokens,
|
|
106
|
-
metadata:
|
|
293
|
+
metadata: metadata
|
|
107
294
|
)
|
|
108
295
|
rescue JSON::ParserError => e
|
|
109
296
|
raise ProviderError.new(
|
|
@@ -112,6 +299,22 @@ module AgentHarness
|
|
|
112
299
|
)
|
|
113
300
|
end
|
|
114
301
|
|
|
302
|
+
def build_streaming_response(accumulated, duration:, model:)
|
|
303
|
+
tool_calls = accumulated[:tool_calls].compact
|
|
304
|
+
metadata = {transport: :http, stream: true}
|
|
305
|
+
metadata[:tool_calls] = tool_calls unless tool_calls.empty?
|
|
306
|
+
|
|
307
|
+
Response.new(
|
|
308
|
+
output: accumulated[:content],
|
|
309
|
+
exit_code: 0,
|
|
310
|
+
duration: duration,
|
|
311
|
+
provider: :claude,
|
|
312
|
+
model: accumulated[:model] || model,
|
|
313
|
+
tokens: accumulated[:usage],
|
|
314
|
+
metadata: metadata
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
|
|
115
318
|
def extract_text_content(body)
|
|
116
319
|
content = body["content"]
|
|
117
320
|
return "" unless content.is_a?(Array)
|
|
@@ -122,6 +325,23 @@ module AgentHarness
|
|
|
122
325
|
.join
|
|
123
326
|
end
|
|
124
327
|
|
|
328
|
+
def extract_tool_calls(body)
|
|
329
|
+
content = body["content"]
|
|
330
|
+
return nil unless content.is_a?(Array)
|
|
331
|
+
|
|
332
|
+
tool_calls = content.filter_map do |block|
|
|
333
|
+
next unless block["type"] == "tool_use"
|
|
334
|
+
|
|
335
|
+
{
|
|
336
|
+
id: block["id"],
|
|
337
|
+
name: block["name"],
|
|
338
|
+
arguments: JSON.generate(block["input"] || {})
|
|
339
|
+
}
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
tool_calls.empty? ? nil : tool_calls
|
|
343
|
+
end
|
|
344
|
+
|
|
125
345
|
def extract_tokens(body)
|
|
126
346
|
usage = body["usage"]
|
|
127
347
|
return nil unless usage
|
|
@@ -133,11 +353,15 @@ module AgentHarness
|
|
|
133
353
|
end
|
|
134
354
|
|
|
135
355
|
def handle_error_response(http_response, status_code)
|
|
356
|
+
handle_error_response_raw(http_response.body, status_code)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def handle_error_response_raw(body_string, status_code)
|
|
136
360
|
message = begin
|
|
137
|
-
body = JSON.parse(
|
|
138
|
-
body.dig("error", "message") || body.dig("error", "type") ||
|
|
361
|
+
body = JSON.parse(body_string)
|
|
362
|
+
body.dig("error", "message") || body.dig("error", "type") || body_string
|
|
139
363
|
rescue JSON::ParserError
|
|
140
|
-
|
|
364
|
+
body_string
|
|
141
365
|
end
|
|
142
366
|
|
|
143
367
|
case status_code
|
|
@@ -164,5 +388,88 @@ module AgentHarness
|
|
|
164
388
|
raise ProviderError.new("HTTP #{status_code}: #{message}")
|
|
165
389
|
end
|
|
166
390
|
end
|
|
391
|
+
|
|
392
|
+
def build_chat_chunk_callback(on_chunk, on_chat_chunk, observer)
|
|
393
|
+
proc do |chunk|
|
|
394
|
+
on_chunk&.call(chunk)
|
|
395
|
+
on_chat_chunk&.call(chunk)
|
|
396
|
+
observer.on_chat_chunk(chunk) if observer_responds_to?(observer, :on_chat_chunk)
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def process_content_block_start(payload, accumulated, &on_chunk)
|
|
401
|
+
content_block = payload["content_block"] || {}
|
|
402
|
+
|
|
403
|
+
case content_block["type"]
|
|
404
|
+
when "text"
|
|
405
|
+
emit_text_delta(content_block["text"], accumulated, &on_chunk)
|
|
406
|
+
when "tool_use"
|
|
407
|
+
index = payload["index"] || 0
|
|
408
|
+
accumulated[:tool_calls][index] = {
|
|
409
|
+
id: content_block["id"],
|
|
410
|
+
name: content_block["name"],
|
|
411
|
+
arguments: +"",
|
|
412
|
+
structured_input: content_block["input"],
|
|
413
|
+
saw_delta: false
|
|
414
|
+
}
|
|
415
|
+
on_chunk.call({
|
|
416
|
+
type: :tool_call_start,
|
|
417
|
+
id: content_block["id"],
|
|
418
|
+
name: content_block["name"]
|
|
419
|
+
})
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def process_content_block_delta(payload, accumulated, &on_chunk)
|
|
424
|
+
delta = payload["delta"] || {}
|
|
425
|
+
|
|
426
|
+
case delta["type"]
|
|
427
|
+
when "text_delta"
|
|
428
|
+
emit_text_delta(delta["text"], accumulated, &on_chunk)
|
|
429
|
+
when "input_json_delta"
|
|
430
|
+
index = payload["index"] || 0
|
|
431
|
+
tool_call = accumulated[:tool_calls][index]
|
|
432
|
+
return unless tool_call
|
|
433
|
+
|
|
434
|
+
partial_json = delta["partial_json"]
|
|
435
|
+
return if partial_json.nil? || partial_json.empty?
|
|
436
|
+
|
|
437
|
+
tool_call[:saw_delta] = true
|
|
438
|
+
tool_call[:arguments] << partial_json
|
|
439
|
+
on_chunk.call({
|
|
440
|
+
type: :tool_call_delta,
|
|
441
|
+
id: tool_call[:id],
|
|
442
|
+
arguments: partial_json
|
|
443
|
+
})
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def process_content_block_stop(payload, accumulated, &on_chunk)
|
|
448
|
+
index = payload["index"] || 0
|
|
449
|
+
tool_call = accumulated[:tool_calls][index]
|
|
450
|
+
return unless tool_call
|
|
451
|
+
|
|
452
|
+
arguments = finalized_tool_call_arguments(tool_call)
|
|
453
|
+
tool_call[:arguments] = arguments
|
|
454
|
+
tool_call.delete(:structured_input)
|
|
455
|
+
tool_call.delete(:saw_delta)
|
|
456
|
+
|
|
457
|
+
on_chunk.call({
|
|
458
|
+
type: :tool_call_complete,
|
|
459
|
+
id: tool_call[:id],
|
|
460
|
+
name: tool_call[:name],
|
|
461
|
+
arguments: arguments
|
|
462
|
+
})
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def finalized_tool_call_arguments(tool_call)
|
|
466
|
+
return tool_call[:arguments] if tool_call[:saw_delta]
|
|
467
|
+
|
|
468
|
+
JSON.generate(tool_call[:structured_input] || {})
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def observer_responds_to?(observer, method_name)
|
|
472
|
+
observer&.respond_to?(method_name)
|
|
473
|
+
end
|
|
167
474
|
end
|
|
168
475
|
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -184,19 +184,43 @@ module AgentHarness
|
|
|
184
184
|
Authentication.auth_status(provider_name)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
+
# Get authentication flow capabilities for a provider
|
|
188
|
+
# @param provider_name [Symbol] the provider name
|
|
189
|
+
# @return [Hash] capabilities with :auth_type, :auth_url, :refresh keys
|
|
190
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
191
|
+
def auth_capabilities(provider_name)
|
|
192
|
+
Authentication.auth_capabilities(provider_name)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check whether OAuth URL generation is supported for a provider
|
|
196
|
+
# @param provider_name [Symbol] the provider name
|
|
197
|
+
# @return [Boolean] true if auth_url can be called for the provider
|
|
198
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
199
|
+
def auth_url_supported?(provider_name)
|
|
200
|
+
Authentication.auth_url_supported?(provider_name)
|
|
201
|
+
end
|
|
202
|
+
|
|
187
203
|
# Generate an OAuth URL for a provider
|
|
188
204
|
# @param provider_name [Symbol] the provider name
|
|
189
205
|
# @return [String] the OAuth authorization URL
|
|
190
|
-
# @raise [
|
|
206
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support OAuth
|
|
191
207
|
def auth_url(provider_name)
|
|
192
208
|
Authentication.auth_url(provider_name)
|
|
193
209
|
end
|
|
194
210
|
|
|
211
|
+
# Check whether credential refresh is supported for a provider
|
|
212
|
+
# @param provider_name [Symbol] the provider name
|
|
213
|
+
# @return [Boolean] true if refresh_auth can be called for the provider
|
|
214
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
215
|
+
def refresh_auth_supported?(provider_name)
|
|
216
|
+
Authentication.refresh_auth_supported?(provider_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
195
219
|
# Refresh authentication credentials for a provider
|
|
196
220
|
# @param provider_name [Symbol] the provider name
|
|
197
221
|
# @param token [String, nil] OAuth token to store
|
|
198
222
|
# @return [Hash] result with :success key
|
|
199
|
-
# @raise [
|
|
223
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support credential refresh
|
|
200
224
|
def refresh_auth(provider_name, token: nil)
|
|
201
225
|
Authentication.refresh_auth(provider_name, token: token)
|
|
202
226
|
end
|
|
@@ -246,6 +270,8 @@ require_relative "agent_harness/response"
|
|
|
246
270
|
require_relative "agent_harness/token_tracker"
|
|
247
271
|
require_relative "agent_harness/error_taxonomy"
|
|
248
272
|
require_relative "agent_harness/text_transport"
|
|
273
|
+
require_relative "agent_harness/openai_compatible_transport"
|
|
274
|
+
require_relative "agent_harness/conversation"
|
|
249
275
|
require_relative "agent_harness/authentication"
|
|
250
276
|
require_relative "agent_harness/provider_health_check"
|
|
251
277
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -102,11 +102,13 @@ files:
|
|
|
102
102
|
- lib/agent_harness/authentication.rb
|
|
103
103
|
- lib/agent_harness/command_executor.rb
|
|
104
104
|
- lib/agent_harness/configuration.rb
|
|
105
|
+
- lib/agent_harness/conversation.rb
|
|
105
106
|
- lib/agent_harness/docker_command_executor.rb
|
|
106
107
|
- lib/agent_harness/error_taxonomy.rb
|
|
107
108
|
- lib/agent_harness/errors.rb
|
|
108
109
|
- lib/agent_harness/execution_preparation.rb
|
|
109
110
|
- lib/agent_harness/mcp_server.rb
|
|
111
|
+
- lib/agent_harness/openai_compatible_transport.rb
|
|
110
112
|
- lib/agent_harness/orchestration/circuit_breaker.rb
|
|
111
113
|
- lib/agent_harness/orchestration/conductor.rb
|
|
112
114
|
- lib/agent_harness/orchestration/health_monitor.rb
|