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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6eb541ae57479b17bdf0a03cf773d1049d4d0335c7efea0ba8d45bf642717361
4
- data.tar.gz: ca088c08d42344691354559cf56b2aac9cde4d320b1bbcaba842f8b9f566722f
3
+ metadata.gz: 79e4e172d70ecba9f4f3249a6d1ee93af4f47ba444991beea77f1a0d152cc0af
4
+ data.tar.gz: '0489867bae0853510e78ff6251aab2980048be0de24f10fdba9c8ef2b816f90f'
5
5
  SHA512:
6
- metadata.gz: 52f5fc4c21a8c29cddeb484f27d2ec70ee7045406e0ef8cc7f19bbc855f8ce8af0c8a98f0ddd48c064c2acf0a7989e500cce2ca71c36945c187f36d9f2b8e411
7
- data.tar.gz: d01a63b81d88011db1914122007046a2d272023fedcb2827a8ffae0aa9605618c4c3364dde449f9a56246ae19a4aceef5e6be06ac87e6892c58e3e93fb4bdfcb
6
+ metadata.gz: 6657b638390c5f3ae19c5100d33e78b8ed8b26292d90bfad5e649479c5e7e4b0619a6c20118c9a68d9fa1f216379d4c2d25f62cea82a4c82b61d858d09ddc309
7
+ data.tar.gz: c52577902c4a2852afcceda0f76da39e1f829a4a191051cd5978934955c577830b6ac4281d95d6b563bf3f8c2bf62732b9efe70a1636797b6af014b43b5596d6
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.44.19"
2
+ ".": "0.44.21"
3
3
  }
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. Tool messages MUST reference exactly one `tool_call_id`.
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.8.6"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.44.19"
4
+ VERSION = "0.44.21"
5
5
  end
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.19
4
+ version: 0.44.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan