agent-harness 0.16.1 → 0.17.1

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: 1c7a73c78312193fa3d7a73f3fa32aa032f9a5c8748dd0a679b2f05abc0853fe
4
- data.tar.gz: ca7ed61365d3df1aaa8c3b48ee0100804e7ff49ad08f4da73cc07ce444934b79
3
+ metadata.gz: 68dcb8a37c35c6ded8f4d01e0f0d6d14e1588ac41a88f2b613fa2fbab9decc79
4
+ data.tar.gz: 6317ce0f0ef2aaadf13dfa39b6897d67a4b453bbb7b7b09db348366d33b8d993
5
5
  SHA512:
6
- metadata.gz: 7bbd40a153f7565617f151d2dea5d7bdded0314b3d144971947ab92699dde8c0851a83ab15eed45f0a4eb34faf30a75274de88434963420b28a68263db1c77fd
7
- data.tar.gz: c97a38b9bdb74397b18a9c37f641e3469be3e0fa9532a8728e2f5a8517b2c14bacd5b4b65441fda0c28f2397ab21f83ead4ceaa0aa17585c02c32b5a22d5d8f8
6
+ metadata.gz: 59989e7197cf596e87ae4ffb87f252419d72f3c9c119de4f210fc4a41c7b3777187a69f1745e65f7a1cefb0010b87f316cd375c673de56e906e4b223364dac44
7
+ data.tar.gz: a691c948ee1fb9f3f6777b3624863b42024ecf85c84b5b320a0e258cf4f7467db7832e703048cee42272cd3e946c020321873788d48de225acaf00eaa8bee757
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.16.1"
2
+ ".": "0.17.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.17.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.17.0...agent-harness/v0.17.1) (2026-05-05)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * 173: Smoke test contract timeout (30s) overrides caller timeout, breaking slow models ([#205](https://github.com/viamin/agent-harness/issues/205)) ([3a1e301](https://github.com/viamin/agent-harness/commit/3a1e301e36ef8957fd440f2782b3b8e4687b473c))
9
+
10
+ ## [0.17.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.16.1...agent-harness/v0.17.0) (2026-05-03)
11
+
12
+
13
+ ### Features
14
+
15
+ * **extensions:** add activity heartbeat support for OpenCode/KiloCode-compatible providers ([#201](https://github.com/viamin/agent-harness/issues/201)) ([4914f6d](https://github.com/viamin/agent-harness/commit/4914f6d7971c55600f268b6886b967b753b25c96))
16
+
3
17
  ## [0.16.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.16.0...agent-harness/v0.16.1) (2026-05-03)
4
18
 
5
19
 
@@ -324,7 +324,18 @@ module AgentHarness
324
324
  # contract[:timeout]. When the provider overrides #smoke_test without
325
325
  # publishing a contract, forward the validated health-check timeout so
326
326
  # the override can honour it instead of running without any limit.
327
- smoke_timeout = smoke_contract ? nil : timeout
327
+ # However, when the caller explicitly provides a timeout that exceeds
328
+ # the contract timeout, honour the caller's intent so slow models
329
+ # are not prematurely killed by the contract default.
330
+ contract_timeout = smoke_contract&.dig(:timeout)
331
+ valid_contract_timeout = contract_timeout.is_a?(Numeric) && contract_timeout.positive?
332
+ smoke_timeout = if valid_contract_timeout && timeout && timeout > contract_timeout
333
+ timeout
334
+ elsif smoke_contract
335
+ nil
336
+ else
337
+ timeout
338
+ end
328
339
  smoke = provider_instance.smoke_test(timeout: smoke_timeout, provider_runtime: provider_runtime)
329
340
  unless smoke[:ok]
330
341
  return build_result(
@@ -1187,6 +1187,39 @@ module AgentHarness
1187
1187
  nil
1188
1188
  end
1189
1189
 
1190
+ # Whether this provider supports activity heartbeat signaling.
1191
+ #
1192
+ # Providers that can emit a container-visible liveness signal during
1193
+ # long-running CLI execution return +true+. Downstream callers use
1194
+ # this to decide whether heartbeat-backed idle timeout is viable
1195
+ # without maintaining provider-name allowlists.
1196
+ #
1197
+ # @return [Boolean] true if the provider can emit heartbeat signals
1198
+ def supports_activity_heartbeat?
1199
+ false
1200
+ end
1201
+
1202
+ # Return structured heartbeat integration wiring for this provider.
1203
+ #
1204
+ # Providers that support activity heartbeat return a Hash describing
1205
+ # how to wire heartbeat file touches into the CLI execution. The
1206
+ # returned contract includes environment variables, execution
1207
+ # preparation (config/plugin file writes), and the heartbeat
1208
+ # granularity so downstream callers can enable heartbeat-backed idle
1209
+ # timeout without hardcoding provider-specific config logic.
1210
+ #
1211
+ # @param heartbeat_file_path [String] absolute path to the heartbeat
1212
+ # file that should be touched on activity
1213
+ # @return [Hash] heartbeat integration contract:
1214
+ # - +:supported+ [Boolean] whether heartbeat is available
1215
+ # - +:env+ [Hash] environment variables to set (empty when unsupported)
1216
+ # - +:preparation+ [ExecutionPreparation, nil] file writes for config/plugin wiring
1217
+ # - +:granularity+ [Symbol, nil] heartbeat event granularity
1218
+ # (+:tool_call+, +:turn+, or +:progress+)
1219
+ def heartbeat_integration(heartbeat_file_path:)
1220
+ {supported: false, env: {}, preparation: nil, granularity: nil}
1221
+ end
1222
+
1190
1223
  private
1191
1224
 
1192
1225
  def classify_smoke_test_message(message)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "shellwords"
4
5
 
5
6
  module AgentHarness
6
7
  module Providers
@@ -129,6 +130,39 @@ module AgentHarness
129
130
  }
130
131
  end
131
132
 
133
+ def supports_activity_heartbeat?
134
+ true
135
+ end
136
+
137
+ def heartbeat_integration(heartbeat_file_path:)
138
+ unless heartbeat_file_path.is_a?(String) && !heartbeat_file_path.strip.empty?
139
+ raise ArgumentError, "heartbeat_file_path must be a non-empty String"
140
+ end
141
+ unless heartbeat_file_path.start_with?("/")
142
+ raise ArgumentError, "heartbeat_file_path must be an absolute path (got #{heartbeat_file_path.inspect})"
143
+ end
144
+
145
+ hook_script = heartbeat_hook_script(heartbeat_file_path)
146
+ config_payload = merge_heartbeat_hooks(hook_script)
147
+
148
+ preparation = ExecutionPreparation.new(
149
+ file_writes: [
150
+ {
151
+ path: heartbeat_hook_config_path,
152
+ content: JSON.pretty_generate(config_payload),
153
+ mode: 0o600
154
+ }
155
+ ]
156
+ )
157
+
158
+ {
159
+ supported: true,
160
+ env: {"KILO_HEARTBEAT_FILE" => heartbeat_file_path},
161
+ preparation: preparation,
162
+ granularity: :tool_call
163
+ }
164
+ end
165
+
132
166
  def config_file_content(options = {})
133
167
  # Only use explicit provider_name or default to "openai".
134
168
  # api_provider is a generic backend label (e.g. "openrouter") that is not
@@ -298,6 +332,32 @@ module AgentHarness
298
332
 
299
333
  private
300
334
 
335
+ def heartbeat_hook_script(heartbeat_file_path)
336
+ "touch #{Shellwords.escape(heartbeat_file_path)}"
337
+ end
338
+
339
+ def heartbeat_hook_config_path
340
+ "~/.config/kilocode/hooks.json"
341
+ end
342
+
343
+ def merge_heartbeat_hooks(hook_script)
344
+ existing = load_existing_hooks_config(heartbeat_hook_config_path)
345
+ hooks = existing.fetch("hooks", {})
346
+ on_activity = hooks.fetch("on_activity", [])
347
+ on_activity = on_activity.dup
348
+ on_activity << {"command" => hook_script}
349
+ existing.merge("hooks" => hooks.merge("on_activity" => on_activity))
350
+ end
351
+
352
+ def load_existing_hooks_config(path)
353
+ expanded = File.expand_path(path)
354
+ return {} unless File.exist?(expanded)
355
+
356
+ JSON.parse(File.read(expanded))
357
+ rescue JSON::ParserError
358
+ {}
359
+ end
360
+
301
361
  def each_json_event(output)
302
362
  return if output.nil? || output.empty?
303
363
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "shellwords"
4
5
 
5
6
  module AgentHarness
6
7
  module Providers
@@ -147,6 +148,39 @@ module AgentHarness
147
148
  }
148
149
  end
149
150
 
151
+ def supports_activity_heartbeat?
152
+ true
153
+ end
154
+
155
+ def heartbeat_integration(heartbeat_file_path:)
156
+ unless heartbeat_file_path.is_a?(String) && !heartbeat_file_path.strip.empty?
157
+ raise ArgumentError, "heartbeat_file_path must be a non-empty String"
158
+ end
159
+ unless heartbeat_file_path.start_with?("/")
160
+ raise ArgumentError, "heartbeat_file_path must be an absolute path (got #{heartbeat_file_path.inspect})"
161
+ end
162
+
163
+ hook_script = heartbeat_hook_script(heartbeat_file_path)
164
+ config_payload = merge_heartbeat_hooks(hook_script)
165
+
166
+ preparation = ExecutionPreparation.new(
167
+ file_writes: [
168
+ {
169
+ path: heartbeat_hook_config_path,
170
+ content: serialize_opencode_config(config_payload),
171
+ mode: 0o600
172
+ }
173
+ ]
174
+ )
175
+
176
+ {
177
+ supported: true,
178
+ env: {"OPENCODE_HEARTBEAT_FILE" => heartbeat_file_path},
179
+ preparation: preparation,
180
+ granularity: :tool_call
181
+ }
182
+ end
183
+
150
184
  def error_patterns
151
185
  COMMON_ERROR_PATTERNS
152
186
  end
@@ -233,6 +267,32 @@ module AgentHarness
233
267
  JSON.pretty_generate(payload)
234
268
  end
235
269
 
270
+ def heartbeat_hook_script(heartbeat_file_path)
271
+ "touch #{Shellwords.escape(heartbeat_file_path)}"
272
+ end
273
+
274
+ def heartbeat_hook_config_path
275
+ "~/.config/opencode/hooks.json"
276
+ end
277
+
278
+ def merge_heartbeat_hooks(hook_script)
279
+ existing = load_existing_hooks_config(heartbeat_hook_config_path)
280
+ hooks = existing.fetch("hooks", {})
281
+ on_activity = hooks.fetch("on_activity", [])
282
+ on_activity = on_activity.dup
283
+ on_activity << {"command" => hook_script}
284
+ existing.merge("hooks" => hooks.merge("on_activity" => on_activity))
285
+ end
286
+
287
+ def load_existing_hooks_config(path)
288
+ expanded = File.expand_path(path)
289
+ return {} unless File.exist?(expanded)
290
+
291
+ JSON.parse(File.read(expanded))
292
+ rescue JSON::ParserError
293
+ {}
294
+ end
295
+
236
296
  def stringify_keys(hash)
237
297
  hash.each_with_object({}) do |(key, value), result|
238
298
  result[key.to_s] = value
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.16.1"
4
+ VERSION = "0.17.1"
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.16.1
4
+ version: 0.17.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan