agent-harness 0.44.19 → 0.44.21
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 +7 -1
- data/lib/agent_harness/api/chat_transport.rb +1 -0
- data/lib/agent_harness/api/ruby_llm_chat_adapter.rb +101 -5
- data/lib/agent_harness/providers/omp.rb +1 -1
- data/lib/agent_harness/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79e4e172d70ecba9f4f3249a6d1ee93af4f47ba444991beea77f1a0d152cc0af
|
|
4
|
+
data.tar.gz: '0489867bae0853510e78ff6251aab2980048be0de24f10fdba9c8ef2b816f90f'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6657b638390c5f3ae19c5100d33e78b8ed8b26292d90bfad5e649479c5e7e4b0619a6c20118c9a68d9fa1f216379d4c2d25f62cea82a4c82b61d858d09ddc309
|
|
7
|
+
data.tar.gz: c52577902c4a2852afcceda0f76da39e1f829a4a191051cd5978934955c577830b6ac4281d95d6b563bf3f8c2bf62732b9efe70a1636797b6af014b43b5596d6
|
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.21](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.20...agent-harness/v0.44.21) (2026-10-21)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* 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))
|
|
21
|
+
|
|
22
|
+
## [0.44.20](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.19...agent-harness/v0.44.20) (2026-10-19)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **deps-dev:** bump @oh-my-pi/pi-coding-agent from 18.8.6 to 18.11.2 in /vendor/pins/omp ([#489](https://github.com/viamin/agent-harness/issues/489)) ([7ea477e](https://github.com/viamin/agent-harness/commit/7ea477e57008a343563aba159ba6aad477337b5a))
|
|
28
|
+
|
|
14
29
|
## [0.44.19](https://github.com/viamin/agent-harness/compare/agent-harness/v0.44.18...agent-harness/v0.44.19) (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
|
|
|
@@ -150,6 +150,7 @@ module AgentHarness
|
|
|
150
150
|
content: schema_result[:content],
|
|
151
151
|
parsed: schema_result[:parsed],
|
|
152
152
|
tool_calls: normalize_tool_calls(adapter_result[:tool_calls]),
|
|
153
|
+
provider_state: adapter_result[:provider_state],
|
|
153
154
|
finish_reason: adapter_result[:finish_reason],
|
|
154
155
|
usage: aggregate_usage,
|
|
155
156
|
error: schema_result[:error]
|
|
@@ -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|
|
|
@@ -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)
|
|
@@ -12,7 +12,7 @@ module AgentHarness
|
|
|
12
12
|
# CLI.
|
|
13
13
|
class OhMyPi < Base
|
|
14
14
|
CLI_PACKAGE = "@oh-my-pi/pi-coding-agent"
|
|
15
|
-
SUPPORTED_CLI_VERSION = "18.
|
|
15
|
+
SUPPORTED_CLI_VERSION = "18.11.2"
|
|
16
16
|
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new("= #{SUPPORTED_CLI_VERSION}").freeze
|
|
17
17
|
|
|
18
18
|
# Bun runtime requirements. The omp entrypoint is
|