agent-harness 0.44.20 → 0.44.22
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 +15 -0
- data/README.md +49 -0
- data/docs/provider-neutral-api-execution-contract.md +14 -2
- data/lib/agent_harness/api/chat_transport.rb +14 -3
- data/lib/agent_harness/api/provider_error_detail.rb +70 -0
- data/lib/agent_harness/api/ruby_llm_chat_adapter.rb +102 -6
- data/lib/agent_harness/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd3556ae8d2b4d460344b7479ad613d978baec38b54a7261bc4024314b2a5fd6
|
|
4
|
+
data.tar.gz: e5db97cdc1f923b2791d80bd3ca7b3dcf1060adc30a0c979f96da94e93740f63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38cba9849f8f085fe92b6c9bb5a7de4df67d4c3591a6d6a48459d391aaf4dc79ccbd8dbc684d667e103df3b2e3661dc82da8e4d88df9327ccf231741c1058525
|
|
7
|
+
data.tar.gz: 2567485b7f2c3f181d61d9124d43c6e6dd13bdf3ac6158687e6b5ebd0f75a8506a9e0b5dc0157ccc4fa4ff9a06ee52483b0f7571c4442de8fc3bdc59cdaef712
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
### Features
|
|
4
4
|
|
|
5
|
+
* support reasoning-capable OpenAI function-tool chat through the Responses API, including opaque reasoning state across tool-result continuation, streaming tool IDs and arguments, usage, cancellation, classified errors, an explicit compatible-provider protocol contract, and a gated live smoke test ([#499](https://github.com/viamin/agent-harness/issues/499)).
|
|
5
6
|
* verify RDR-072's published capability artifacts and migration limits, retain the application chat loop over normalized transport, and remove the private RubyLLM streaming-refusal patch in favor of an explicit non-streaming schema boundary ([#437](https://github.com/viamin/agent-harness/issues/437)).
|
|
6
7
|
* expose normalized API chat transport and schema-constrained responses with preserved JSON text, locally validated parsed values, and explicit refusal, truncation, invalid JSON, schema mismatch, and unsupported-mode outcomes; this combined delivery supersedes the separate chat-transport work item ([#433](https://github.com/viamin/agent-harness/issues/433), [#434](https://github.com/viamin/agent-harness/issues/434)).
|
|
7
8
|
* add runner model compatibility contract (`AgentHarness.model_compatibility`) with structured `ModelCompatibility::Result` outcomes. Codex exposes static facts for CLI-gated models (e.g. `gpt-5.5` requires Codex CLI `>= 0.116.0`), a baseline supported-model list, supported auth modes, and a `DEFAULT_COMPATIBLE_MODEL_ID` fallback so downstream orchestrators can validate tier/model assignments before scheduling agent runs ([#259](https://github.com/viamin/agent-harness/issues/259)).
|
|
@@ -11,6 +12,20 @@
|
|
|
11
12
|
|
|
12
13
|
* close the RDR-072 upstream epic from #437's retained-scope evidence while keeping unpublished capabilities and downstream adoption behind their explicit release and integration gates ([#430](https://github.com/viamin/agent-harness/issues/430)).
|
|
13
14
|
|
|
15
|
+
## [0.44.22](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.21...agent-harness/v0.44.22) (2026-10-21)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* Preserve safe actionable provider error details in API chat failures ([#502](https://github.com/viamin/agent-harness/issues/502)) ([509a877](https://github.com/viamin/agent-harness/commit/509a8773e330cdc4bccb53edfe51bafef6b8e20f))
|
|
21
|
+
|
|
22
|
+
## [0.44.21](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.20...agent-harness/v0.44.21) (2026-10-21)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* Support reasoning-capable OpenAI chat with function tools through Responses API ([#501](https://github.com/viamin/agent-harness/issues/501)) ([5976a89](https://github.com/viamin/agent-harness/commit/5976a89b2dacec48338eac8b2b45bc59976e4bd1))
|
|
28
|
+
|
|
14
29
|
## [0.44.20](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.19...agent-harness/v0.44.20) (2026-10-19)
|
|
15
30
|
|
|
16
31
|
|
data/README.md
CHANGED
|
@@ -104,6 +104,55 @@ agent-harness release containing this capability:
|
|
|
104
104
|
The runtime dependency is Ruby 3.2 or newer and RubyLLM 2.x. No Rails database
|
|
105
105
|
or RubyLLM persistence tables are required for this plain-Ruby operation.
|
|
106
106
|
|
|
107
|
+
## Native API chat
|
|
108
|
+
|
|
109
|
+
`AgentHarness::Api::ChatTransport` supports OpenAI's Responses API explicitly.
|
|
110
|
+
Select `protocol: :responses` for reasoning-capable OpenAI models used with
|
|
111
|
+
function tools; this preserves the model's reasoning behavior. Do not select
|
|
112
|
+
Chat Completions and do not disable reasoning or remove tools as a workaround.
|
|
113
|
+
OpenAI-compatible providers that only implement Chat Completions continue to
|
|
114
|
+
use `provider: :openai`, their explicit `endpoint`, and
|
|
115
|
+
`protocol: :chat_completions`; Agent Harness never probes or silently changes
|
|
116
|
+
the selected protocol.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
candidate = {
|
|
120
|
+
provider: :openai,
|
|
121
|
+
model: "gpt-5.6-luna",
|
|
122
|
+
protocol: :responses,
|
|
123
|
+
authentication_mode: :api_key,
|
|
124
|
+
credentials: {api_key: ENV.fetch("OPENAI_API_KEY")}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
result = AgentHarness::Api::ChatTransport.new.call(
|
|
128
|
+
request_id: SecureRandom.uuid,
|
|
129
|
+
operation: :chat,
|
|
130
|
+
candidates: [candidate],
|
|
131
|
+
messages: [{role: :user, content: "Look up order 42"}],
|
|
132
|
+
tools: [{
|
|
133
|
+
name: "lookup_order",
|
|
134
|
+
description: "Find an order",
|
|
135
|
+
input_schema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]}
|
|
136
|
+
}],
|
|
137
|
+
stream: true,
|
|
138
|
+
retry: {max_attempts: 1}
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
The harness returns tool calls without executing them. For the next request,
|
|
143
|
+
append the assistant message (including `tool_calls` and `provider_state`) and
|
|
144
|
+
then a tool message whose `tool_call_id` is the returned stable harness ID.
|
|
145
|
+
`provider_state` is opaque protocol continuation data: persist and return it
|
|
146
|
+
unchanged, and never inspect or log it. The transport maps the stable ID back
|
|
147
|
+
to the provider call ID and replays the encrypted Responses reasoning item.
|
|
148
|
+
|
|
149
|
+
To run the credential-gated live tool-continuation smoke test:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
AGENT_HARNESS_LIVE_OPENAI=1 OPENAI_API_KEY=... \
|
|
153
|
+
bundle exec rspec spec/live/openai_responses_reasoning_spec.rb
|
|
154
|
+
```
|
|
155
|
+
|
|
107
156
|
## Configuration
|
|
108
157
|
|
|
109
158
|
### Ruby DSL
|
|
@@ -247,7 +247,10 @@ The input is an ordered array. Each message has a stable caller-owned `id`, one
|
|
|
247
247
|
of `system`, `user`, `assistant`, or `tool` as its `role`, and an ordered
|
|
248
248
|
`content` array. Content parts initially support `text`; unimplemented media
|
|
249
249
|
parts produce an explicit unsupported outcome. Assistant messages MAY contain
|
|
250
|
-
tool calls.
|
|
250
|
+
tool calls. A Responses assistant message that requests tools also carries the
|
|
251
|
+
opaque `provider_state` returned by the preceding result. Consumers MUST
|
|
252
|
+
persist and return that value unchanged and MUST NOT inspect or log it. Tool
|
|
253
|
+
messages MUST reference exactly one `tool_call_id`.
|
|
251
254
|
|
|
252
255
|
Provider-specific wire roles, system-message placement, and content block
|
|
253
256
|
shapes stay behind the harness boundary. The harness MUST preserve ordering,
|
|
@@ -275,6 +278,9 @@ reference the stable ID. Redelivery or restoration MUST skip a tool call when a
|
|
|
275
278
|
completed result for that ID exists.
|
|
276
279
|
|
|
277
280
|
The transport never decides approval and never executes application tools.
|
|
281
|
+
For stateless OpenAI Responses continuation, the harness uses `provider_state`
|
|
282
|
+
to replay the provider's encrypted reasoning and function-call output items.
|
|
283
|
+
The field is absent when the selected protocol does not require opaque state.
|
|
278
284
|
|
|
279
285
|
### Streaming
|
|
280
286
|
|
|
@@ -484,7 +490,13 @@ result/usage where available. Categories are:
|
|
|
484
490
|
|
|
485
491
|
Adapters MUST use the following category and code mappings. A provider-specific
|
|
486
492
|
status or error name may be retained as sanitized metadata, but MUST NOT replace
|
|
487
|
-
these values or change fallback eligibility.
|
|
493
|
+
these values or change fallback eligibility. The normalized `message` may append
|
|
494
|
+
bounded, actionable detail for request, capability, configuration, and
|
|
495
|
+
response-shape failures. Such detail MUST redact credentials, headers, request
|
|
496
|
+
bodies, conversation content, and URLs, and MUST never copy an unparsed provider
|
|
497
|
+
response body or a message derived from one. Parser diagnostics are also omitted
|
|
498
|
+
because they can quote request or conversation content. Attempt reports retain
|
|
499
|
+
the same sanitized error object as the public result.
|
|
488
500
|
|
|
489
501
|
| Failure | Category | Code |
|
|
490
502
|
| --- | --- | --- |
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
require "ruby_llm"
|
|
5
5
|
require_relative "attempt_report"
|
|
6
|
+
require_relative "provider_error_detail"
|
|
6
7
|
require_relative "ruby_llm_chat_adapter"
|
|
7
8
|
require_relative "schema_response"
|
|
8
9
|
|
|
@@ -150,6 +151,7 @@ module AgentHarness
|
|
|
150
151
|
content: schema_result[:content],
|
|
151
152
|
parsed: schema_result[:parsed],
|
|
152
153
|
tool_calls: normalize_tool_calls(adapter_result[:tool_calls]),
|
|
154
|
+
provider_state: adapter_result[:provider_state],
|
|
153
155
|
finish_reason: adapter_result[:finish_reason],
|
|
154
156
|
usage: aggregate_usage,
|
|
155
157
|
error: schema_result[:error]
|
|
@@ -442,6 +444,11 @@ module AgentHarness
|
|
|
442
444
|
end
|
|
443
445
|
|
|
444
446
|
module ErrorClassifier
|
|
447
|
+
ACTIONABLE_DETAIL_ERRORS = [
|
|
448
|
+
RubyLLM::BadRequestError,
|
|
449
|
+
RubyLlmChatAdapter::UnsupportedOptionError
|
|
450
|
+
].freeze
|
|
451
|
+
|
|
445
452
|
TRANSIENT = {
|
|
446
453
|
Faraday::TimeoutError => :timeout,
|
|
447
454
|
Faraday::ConnectionFailed => :connection_failed,
|
|
@@ -479,7 +486,7 @@ module AgentHarness
|
|
|
479
486
|
end
|
|
480
487
|
|
|
481
488
|
def self.payload(error, category, code, retryable)
|
|
482
|
-
payload = {category: category, code: code, retryable: retryable, message: safe_message(category, code)}
|
|
489
|
+
payload = {category: category, code: code, retryable: retryable, message: safe_message(error, category, code)}
|
|
483
490
|
retry_after = retry_after_seconds(error)
|
|
484
491
|
payload[:retry_after_seconds] = retry_after if retry_after
|
|
485
492
|
payload
|
|
@@ -496,8 +503,12 @@ module AgentHarness
|
|
|
496
503
|
end
|
|
497
504
|
private_class_method :retry_after_seconds
|
|
498
505
|
|
|
499
|
-
def self.safe_message(category, code)
|
|
500
|
-
"Chat request failed (#{category}/#{code})"
|
|
506
|
+
def self.safe_message(error, category, code)
|
|
507
|
+
summary = "Chat request failed (#{category}/#{code})"
|
|
508
|
+
return summary unless ACTIONABLE_DETAIL_ERRORS.any? { |klass| error.is_a?(klass) }
|
|
509
|
+
|
|
510
|
+
detail = ProviderErrorDetail.call(error)
|
|
511
|
+
detail.to_s.empty? ? summary : "#{summary}: #{detail}"
|
|
501
512
|
end
|
|
502
513
|
private_class_method :safe_message
|
|
503
514
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
module Api
|
|
5
|
+
# Extracts allowlisted, actionable text from provider errors without
|
|
6
|
+
# retaining response bodies or caller-controlled message content.
|
|
7
|
+
module ProviderErrorDetail
|
|
8
|
+
LOCAL_DETAILS = [
|
|
9
|
+
"RubyLLM does not support request-local connect timeouts",
|
|
10
|
+
"provider state does not match the selected protocol",
|
|
11
|
+
"provider state must carry at least one Responses output item",
|
|
12
|
+
"unsupported content type"
|
|
13
|
+
].freeze
|
|
14
|
+
TOOL_REASONING_DETAIL = "Function tools with reasoning_effort are not supported for gpt-5.6-luna " \
|
|
15
|
+
"in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'."
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def call(error)
|
|
20
|
+
message = normalized(error.message)
|
|
21
|
+
return if message.empty? || structured_body?(message) || !safe_source?(error, message)
|
|
22
|
+
|
|
23
|
+
message
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def normalized(value)
|
|
27
|
+
value.to_s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "�")
|
|
28
|
+
.gsub(/[[:cntrl:]]+/, " ").gsub(/\s+/, " ").strip
|
|
29
|
+
end
|
|
30
|
+
private_class_method :normalized
|
|
31
|
+
|
|
32
|
+
def safe_source?(error, message)
|
|
33
|
+
safe_local_detail?(error, message) || safe_provider_detail?(error, message)
|
|
34
|
+
end
|
|
35
|
+
private_class_method :safe_source?
|
|
36
|
+
|
|
37
|
+
def safe_local_detail?(error, message)
|
|
38
|
+
error.is_a?(RubyLlmChatAdapter::UnsupportedOptionError) && LOCAL_DETAILS.include?(message)
|
|
39
|
+
end
|
|
40
|
+
private_class_method :safe_local_detail?
|
|
41
|
+
|
|
42
|
+
# RubyLLM parses this provider diagnostic from a JSON error field before
|
|
43
|
+
# constructing BadRequestError. Checking both the structured body and its
|
|
44
|
+
# fixed, non-content-bearing shape avoids exposing raw bodies or arbitrary
|
|
45
|
+
# provider prose that may echo request or conversation content.
|
|
46
|
+
def safe_provider_detail?(error, message)
|
|
47
|
+
error.is_a?(RubyLLM::BadRequestError) && message == TOOL_REASONING_DETAIL &&
|
|
48
|
+
structured_messages(error.response).include?(message)
|
|
49
|
+
end
|
|
50
|
+
private_class_method :safe_provider_detail?
|
|
51
|
+
|
|
52
|
+
def structured_messages(response)
|
|
53
|
+
body = response&.body
|
|
54
|
+
parsed = body.is_a?(String) ? JSON.parse(body) : body
|
|
55
|
+
return [] unless parsed.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
error = parsed["error"] || parsed[:error]
|
|
58
|
+
[error, parsed].filter_map { |part| part["message"] || part[:message] if part.is_a?(Hash) }
|
|
59
|
+
rescue JSON::ParserError
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
private_class_method :structured_messages
|
|
63
|
+
|
|
64
|
+
def structured_body?(message)
|
|
65
|
+
message.start_with?("{", "[", "<")
|
|
66
|
+
end
|
|
67
|
+
private_class_method :structured_body?
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -26,8 +26,8 @@ module AgentHarness
|
|
|
26
26
|
provider_usage_reported = true if event[:type] == :usage_updated
|
|
27
27
|
on_event&.call(event)
|
|
28
28
|
end
|
|
29
|
-
emit_completed_tool_calls(response, &on_event) if stream
|
|
30
|
-
normalize_response(response)
|
|
29
|
+
emit_completed_tool_calls(response, &on_event) if stream && on_event
|
|
30
|
+
normalize_response(response, candidate)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def prepare(candidate:, messages:, tools:, max_output_tokens:, temperature:, timeout:, on_accounting: nil,
|
|
@@ -74,6 +74,66 @@ module AgentHarness
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Captures the opaque Responses output items that continue a turn.
|
|
78
|
+
# RubyLLM exposes the provider output only when it kept it: an
|
|
79
|
+
# unstreamed call parses the response body, and a streamed one keeps the
|
|
80
|
+
# output for namespaced calls and server tools. A streamed turn that used
|
|
81
|
+
# plain function calls leaves neither, so its replay items are rebuilt
|
|
82
|
+
# from the reasoning signature and tool calls the stream accumulator
|
|
83
|
+
# retained, in the same shape the Responses protocol itself replays.
|
|
84
|
+
class ResponsesOutput
|
|
85
|
+
def self.from(message)
|
|
86
|
+
new(message).items
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def initialize(message)
|
|
90
|
+
@message = message
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def items
|
|
94
|
+
retained_output || replay_items
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def retained_output
|
|
100
|
+
[parsed_body_output, @message.raw_content].find { |output| output.is_a?(Array) && output.any? }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# A streamed response's raw is the Faraday response, whose body the
|
|
104
|
+
# adapter empties, so only an unstreamed body is a parsed Hash here.
|
|
105
|
+
def parsed_body_output
|
|
106
|
+
body = @message.raw&.body
|
|
107
|
+
body["output"] if body.is_a?(Hash)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def replay_items
|
|
111
|
+
[reasoning_item, message_item, *function_call_items].compact
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def reasoning_item
|
|
115
|
+
thinking = @message.thinking
|
|
116
|
+
return unless thinking&.signature
|
|
117
|
+
|
|
118
|
+
summary = thinking.text.to_s.empty? ? [] : [{type: "summary_text", text: thinking.text}]
|
|
119
|
+
{"type" => "reasoning", "summary" => summary, "encrypted_content" => thinking.signature}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def message_item
|
|
123
|
+
content = @message.content.to_s
|
|
124
|
+
return if content.strip.empty?
|
|
125
|
+
|
|
126
|
+
{"role" => "assistant", "content" => [{"type" => "output_text", "text" => content}]}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def function_call_items
|
|
130
|
+
Array(@message.tool_calls&.values).map do |call|
|
|
131
|
+
{"type" => "function_call", "call_id" => call.id, "name" => call.name,
|
|
132
|
+
"arguments" => JSON.generate(call.arguments || {})}
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
77
137
|
private
|
|
78
138
|
|
|
79
139
|
def ruby_llm_protocol(candidate)
|
|
@@ -151,7 +211,7 @@ module AgentHarness
|
|
|
151
211
|
end
|
|
152
212
|
|
|
153
213
|
def configure_chat(chat, candidate, messages, tools, max_output_tokens, temperature, schema)
|
|
154
|
-
chat.messages = normalize_messages(messages)
|
|
214
|
+
chat.messages = normalize_messages(messages, candidate)
|
|
155
215
|
chat.with_tools(tools.map { |tool| normalized_tool(tool) }) unless tools.empty?
|
|
156
216
|
chat.with_headers(candidate[:headers] || {})
|
|
157
217
|
chat.with_max_output_tokens(max_output_tokens) if max_output_tokens
|
|
@@ -185,17 +245,43 @@ module AgentHarness
|
|
|
185
245
|
token.respond_to?(:cancelled?) ? token.cancelled? : token.call
|
|
186
246
|
end
|
|
187
247
|
|
|
188
|
-
def normalize_messages(messages)
|
|
248
|
+
def normalize_messages(messages, candidate)
|
|
189
249
|
stable_to_provider = tool_id_map(messages)
|
|
190
250
|
messages.map do |message|
|
|
191
251
|
role = message.fetch(:role).to_sym
|
|
192
252
|
normalized = {role: role, content: text_content(message[:content])}
|
|
193
253
|
normalized[:tool_calls] = normalize_input_tool_calls(message[:tool_calls]) if message[:tool_calls]
|
|
194
254
|
normalized[:tool_call_id] = stable_to_provider.fetch(message[:tool_call_id], message[:tool_call_id]) if role == :tool
|
|
255
|
+
apply_provider_state(normalized, message[:provider_state], candidate) if role == :assistant
|
|
195
256
|
normalized
|
|
196
257
|
end
|
|
197
258
|
end
|
|
198
259
|
|
|
260
|
+
def apply_provider_state(message, state, candidate)
|
|
261
|
+
return unless state
|
|
262
|
+
|
|
263
|
+
validate_provider_state(state, candidate)
|
|
264
|
+
message[:raw_content] = state[:output]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Provider state crosses a caller-owned boundary, so anything the
|
|
268
|
+
# selected protocol cannot replay as an assistant turn is rejected here
|
|
269
|
+
# rather than sent: an empty item list would drop the message content and
|
|
270
|
+
# its tool calls from the request.
|
|
271
|
+
def validate_provider_state(state, candidate)
|
|
272
|
+
unless responses_protocol?(candidate) && state[:protocol]&.to_sym == :responses
|
|
273
|
+
raise UnsupportedOptionError, "provider state does not match the selected protocol"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
return if state[:output].is_a?(Array) && state[:output].any?
|
|
277
|
+
|
|
278
|
+
raise UnsupportedOptionError, "provider state must carry at least one Responses output item"
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def responses_protocol?(candidate)
|
|
282
|
+
candidate[:provider].to_s == "openai" && candidate[:protocol].to_s == "responses"
|
|
283
|
+
end
|
|
284
|
+
|
|
199
285
|
def tool_id_map(messages)
|
|
200
286
|
messages.each_with_object({}) do |message, ids|
|
|
201
287
|
Array(message[:tool_calls]).each do |call|
|
|
@@ -217,7 +303,7 @@ module AgentHarness
|
|
|
217
303
|
|
|
218
304
|
Array(content).map do |part|
|
|
219
305
|
unless part[:type]&.to_sym == :text
|
|
220
|
-
raise UnsupportedOptionError, "unsupported content type
|
|
306
|
+
raise UnsupportedOptionError, "unsupported content type"
|
|
221
307
|
end
|
|
222
308
|
|
|
223
309
|
part[:text].to_s
|
|
@@ -301,17 +387,27 @@ module AgentHarness
|
|
|
301
387
|
end
|
|
302
388
|
end
|
|
303
389
|
|
|
304
|
-
def normalize_response(response)
|
|
390
|
+
def normalize_response(response, candidate)
|
|
305
391
|
{
|
|
306
392
|
content: response.content || "",
|
|
307
393
|
model: response.model,
|
|
308
394
|
finish_reason: response.finish_reason,
|
|
309
395
|
usage: normalize_usage(response.tokens),
|
|
310
396
|
tool_calls: Array(response.tool_calls&.values).map { |call| normalize_tool_call(call) },
|
|
397
|
+
provider_state: provider_state(response, candidate),
|
|
311
398
|
refusal: refusal?(response)
|
|
312
399
|
}
|
|
313
400
|
end
|
|
314
401
|
|
|
402
|
+
# Only the Responses protocol carries opaque continuation state; other
|
|
403
|
+
# protocols replay a turn from the normalized messages alone.
|
|
404
|
+
def provider_state(response, candidate)
|
|
405
|
+
return unless responses_protocol?(candidate)
|
|
406
|
+
|
|
407
|
+
output = ResponsesOutput.from(response)
|
|
408
|
+
{protocol: :responses, output: output} if output.any?
|
|
409
|
+
end
|
|
410
|
+
|
|
315
411
|
def refusal?(response)
|
|
316
412
|
return true if response.finish_reason == :content_filter
|
|
317
413
|
return false unless response.respond_to?(:raw)
|
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.44.
|
|
4
|
+
version: 0.44.22
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -145,6 +145,7 @@ files:
|
|
|
145
145
|
- lib/agent_harness.rb
|
|
146
146
|
- lib/agent_harness/api/attempt_report.rb
|
|
147
147
|
- lib/agent_harness/api/chat_transport.rb
|
|
148
|
+
- lib/agent_harness/api/provider_error_detail.rb
|
|
148
149
|
- lib/agent_harness/api/ruby_llm_chat_adapter.rb
|
|
149
150
|
- lib/agent_harness/api/schema_response.rb
|
|
150
151
|
- lib/agent_harness/authentication.rb
|