agent-harness 0.15.0 → 0.16.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: 34ea90d66e03bff4f53fe144d888ab33909d80e9a198a9a53f7dac00c0ed5d52
4
- data.tar.gz: 4daf550bb940f4176b06ce186658803e090a90ed2e5811ae7f22d61dc95391b8
3
+ metadata.gz: 1c7a73c78312193fa3d7a73f3fa32aa032f9a5c8748dd0a679b2f05abc0853fe
4
+ data.tar.gz: ca7ed61365d3df1aaa8c3b48ee0100804e7ff49ad08f4da73cc07ce444934b79
5
5
  SHA512:
6
- metadata.gz: 9273ea29e29a8380e9f64a926019bac4adc160b2205aecceec2d5ba8e0837d16b58ab9106f05e61832153de95c03c9c7a58b1458588a4f4552a0218c210a4c4a
7
- data.tar.gz: 0ccefa077c32912d9e6dd205d20420d5ba0170fcfa763df22ca65902ea7fcb9ba600c439e91c38c398fc221dc3b41b9662ce5b693e418078dfd1116027378743
6
+ metadata.gz: 7bbd40a153f7565617f151d2dea5d7bdded0314b3d144971947ab92699dde8c0851a83ab15eed45f0a4eb34faf30a75274de88434963420b28a68263db1c77fd
7
+ data.tar.gz: c97a38b9bdb74397b18a9c37f641e3469be3e0fa9532a8728e2f5a8517b2c14bacd5b4b65441fda0c28f2397ab21f83ead4ceaa0aa17585c02c32b5a22d5d8f8
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.15.0"
2
+ ".": "0.16.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.16.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.16.0...agent-harness/v0.16.1) (2026-05-03)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **kilocode:** config_file_content generates invalid JSON for kilo CLI v7.1.3 ([#190](https://github.com/viamin/agent-harness/issues/190)) ([0f7e24a](https://github.com/viamin/agent-harness/commit/0f7e24a8c342045d16a9332385e93f0607d0845e))
9
+
10
+ ## [0.16.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.15.0...agent-harness/v0.16.0) (2026-05-03)
11
+
12
+
13
+ ### Features
14
+
15
+ * Add plan-only / dry-run API returning command+env without execution ([#192](https://github.com/viamin/agent-harness/issues/192)) ([0e6a105](https://github.com/viamin/agent-harness/commit/0e6a1053515e0495900b67c5845a2f95c571f055))
16
+
3
17
  ## [0.15.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.14.1...agent-harness/v0.15.0) (2026-05-03)
4
18
 
5
19
 
@@ -775,6 +775,15 @@ module AgentHarness
775
775
  raise NotImplementedError, "#{self.class} must implement #send_message"
776
776
  end
777
777
 
778
+ # Return the provider CLI execution plan without executing the command.
779
+ #
780
+ # @param prompt [String] the prompt to send
781
+ # @param options [Hash] provider-specific options
782
+ # @return [Hash] with :command, :env, and :preparation keys
783
+ def plan_execution(prompt:, **options)
784
+ raise NotImplementedError, "#{self.class} must implement #plan_execution"
785
+ end
786
+
778
787
  # Provider configuration schema for app-driven setup UIs
779
788
  #
780
789
  # Returns metadata describing the configurable fields, supported
@@ -263,6 +263,26 @@ module AgentHarness
263
263
  cleanup_llm_history_file!(llm_history_path)
264
264
  end
265
265
 
266
+ def plan_execution(prompt:, **options)
267
+ log_debug("plan_execution_start", prompt_length: prompt.length, options: options.keys)
268
+
269
+ options = normalize_provider_runtime(options)
270
+ options = normalize_mcp_servers(options)
271
+ validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
272
+
273
+ llm_history_path = generate_llm_history_path
274
+
275
+ {
276
+ command: build_command(prompt, options.merge(llm_history_path: llm_history_path)),
277
+ env: build_env(options),
278
+ preparation: build_execution_preparation(options)
279
+ }
280
+ rescue McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
281
+ raise
282
+ rescue => e
283
+ handle_error(e, prompt: prompt, options: options)
284
+ end
285
+
266
286
  # Parse raw container output into a Response.
267
287
  #
268
288
  # Overrides the base implementation to support the
@@ -386,6 +386,17 @@ module AgentHarness
386
386
  cleanup_mcp_tempfiles!
387
387
  end
388
388
 
389
+ def plan_execution(prompt:, **options)
390
+ if options[:mode] == :text
391
+ raise ProviderError,
392
+ "Anthropic text mode uses the HTTP transport and does not produce a CLI execution plan"
393
+ end
394
+
395
+ super
396
+ ensure
397
+ cleanup_mcp_tempfiles!
398
+ end
399
+
389
400
  def api_key_env_var_names = ["ANTHROPIC_API_KEY"]
390
401
 
391
402
  def api_key_unset_vars = ["ANTHROPIC_BASE_URL", "ANTHROPIC_HEADER_X_AGENT_RUN_ID", "ANTHROPIC_HEADER_X_PROXY_TOKEN"]
@@ -197,6 +197,56 @@ module AgentHarness
197
197
  handle_error(e, prompt: prompt, options: options)
198
198
  end
199
199
 
200
+ # Return the provider CLI execution plan without executing it.
201
+ #
202
+ # @param prompt [String] the prompt to send
203
+ # @param options [Hash] additional options
204
+ # @return [Hash] with :command, :env, and :preparation keys
205
+ def plan_execution(prompt:, **options)
206
+ log_debug("plan_execution_start", prompt_length: prompt.length, options: options.keys)
207
+
208
+ if options[:mode] == :text && !supports_text_mode?
209
+ log_debug("text_mode_cli_fallback", provider: self.class.provider_name)
210
+ options = options.except(:mode).merge(tools: :none)
211
+ end
212
+
213
+ if options[:tools] && !supports_tool_control?
214
+ log_debug("tools_option_unsupported",
215
+ provider: self.class.provider_name,
216
+ tools: options[:tools])
217
+ @logger&.warn(
218
+ "[AgentHarness::#{self.class.provider_name}] tools option is not supported " \
219
+ "by this provider and will be ignored"
220
+ )
221
+ end
222
+
223
+ options = normalize_provider_runtime(options)
224
+
225
+ extension_context = apply_extensions_to_prompt(prompt, options)
226
+ prompt = extension_context.prompt
227
+ options = extension_context.options
228
+ options = normalize_sub_agent(options)
229
+ prompt = apply_sub_agent_to_prompt(prompt, options[:translated_sub_agent])
230
+
231
+ options = normalize_mcp_servers(options)
232
+ validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
233
+
234
+ {
235
+ command: build_command(prompt, options),
236
+ env: build_env(options),
237
+ preparation: build_execution_preparation(options)
238
+ }
239
+ rescue ExtensionCompatibilityError, McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
240
+ raise
241
+ rescue => e
242
+ handle_error(e, prompt: prompt, options: options)
243
+ ensure
244
+ # build_command may call build_mcp_flags which creates tempfiles (and
245
+ # in Docker even invokes the executor) via write_mcp_config_file.
246
+ # Clean up so that planning has no lasting side effects.
247
+ cleanup_mcp_tempfiles! if respond_to?(:cleanup_mcp_tempfiles!, true)
248
+ end
249
+
200
250
  # Send a multi-turn chat message via the provider's chat transport.
201
251
  #
202
252
  # Providers that support chat mode can accept either +conversation:+
@@ -312,6 +312,28 @@ module AgentHarness
312
312
  handle_error(e, prompt: prompt, options: options)
313
313
  end
314
314
 
315
+ def plan_execution(prompt:, **options)
316
+ log_debug("plan_execution_start", prompt_length: prompt.length, options: options.keys)
317
+
318
+ options = normalize_provider_runtime(options)
319
+ options = normalize_mcp_servers(options)
320
+ validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
321
+
322
+ runtime = options[:provider_runtime]
323
+ cmd = [self.class.binary_name, "-p"]
324
+ cmd.concat(runtime.flags) if runtime&.flags&.any?
325
+
326
+ {
327
+ command: cmd,
328
+ env: build_env(options),
329
+ preparation: build_execution_preparation(options)
330
+ }
331
+ rescue McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
332
+ raise
333
+ rescue => e
334
+ handle_error(e, prompt: prompt, options: options)
335
+ end
336
+
315
337
  protected
316
338
 
317
339
  def build_command(prompt, options)
@@ -188,11 +188,11 @@ module AgentHarness
188
188
  ["--allow-all"]
189
189
  end
190
190
 
191
- def supports_sessions?(probe_timeout: nil, env: {}, version: nil)
191
+ def supports_sessions?(probe_timeout: nil, env: {}, version: :not_provided)
192
192
  legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
193
193
  end
194
194
 
195
- def session_flags(session_id, version: nil, probe_timeout: nil, env: {})
195
+ def session_flags(session_id, version: :not_provided, probe_timeout: nil, env: {})
196
196
  return [] unless session_id && !session_id.empty?
197
197
  return [] unless legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
198
198
 
@@ -345,6 +345,30 @@ module AgentHarness
345
345
  handle_error(e, prompt: prompt, options: options)
346
346
  end
347
347
 
348
+ def plan_execution(prompt:, **options)
349
+ log_debug("plan_execution_start", prompt_length: prompt.length, options: options.keys)
350
+
351
+ options = normalize_provider_runtime(options)
352
+ options = normalize_mcp_servers(options)
353
+ validate_mcp_servers!(options[:mcp_servers]) if options[:mcp_servers]&.any?
354
+
355
+ env = build_env(options)
356
+ version = planned_copilot_cli_version(env)
357
+ raise unsupported_subcommand_cli_error if subcommand_cli_version?(version)
358
+
359
+ options = options.merge(_command_env: env, _planned_cli_version: version)
360
+
361
+ {
362
+ command: build_command(prompt, options),
363
+ env: env,
364
+ preparation: build_execution_preparation(options)
365
+ }
366
+ rescue McpConfigurationError, McpUnsupportedError, McpTransportUnsupportedError
367
+ raise
368
+ rescue => e
369
+ handle_error(e, prompt: prompt, options: options)
370
+ end
371
+
348
372
  # Parse raw container output into a Response.
349
373
  #
350
374
  # Overrides the base implementation to support the
@@ -377,7 +401,14 @@ module AgentHarness
377
401
  def build_command(prompt, options)
378
402
  env = options.fetch(:_command_env) { build_env(options) }
379
403
  runtime = options[:provider_runtime]
380
- version = copilot_cli_version(probe_timeout: options[:_version_probe_timeout], env: env)
404
+ version = if options.key?(:_planned_cli_version)
405
+ options[:_planned_cli_version]
406
+ else
407
+ copilot_cli_version(
408
+ probe_timeout: options[:_version_probe_timeout],
409
+ env: env
410
+ )
411
+ end
381
412
 
382
413
  raise unsupported_subcommand_cli_error if subcommand_cli_version?(version)
383
414
 
@@ -440,13 +471,13 @@ module AgentHarness
440
471
  ["--allow-all-tools"]
441
472
  end
442
473
 
443
- def supports_json_output_format?(probe_timeout: nil, env: {}, version: nil)
444
- version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
474
+ def supports_json_output_format?(probe_timeout: nil, env: {}, version: :not_provided)
475
+ version = copilot_cli_version(probe_timeout: probe_timeout, env: env) if version == :not_provided
445
476
  !version.nil? && !subcommand_cli_version?(version) && version >= JSON_OUTPUT_MIN_VERSION
446
477
  end
447
478
 
448
- def legacy_prompt_cli?(probe_timeout: nil, env: {}, version: nil)
449
- version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
479
+ def legacy_prompt_cli?(probe_timeout: nil, env: {}, version: :not_provided)
480
+ version = copilot_cli_version(probe_timeout: probe_timeout, env: env) if version == :not_provided
450
481
  !version.nil? && !subcommand_cli_version?(version)
451
482
  end
452
483
 
@@ -475,6 +506,17 @@ module AgentHarness
475
506
  @copilot_cli_versions[cache_key] = nil if defined?(cache_key)
476
507
  end
477
508
 
509
+ def planned_copilot_cli_version(env)
510
+ cache_key = version_probe_cache_key(env)
511
+ @copilot_cli_versions ||= {}
512
+ return @copilot_cli_versions[cache_key] if @copilot_cli_versions.key?(cache_key)
513
+
514
+ # When no cached version is available (cold start), return nil so
515
+ # build_command falls back to the conservative -s flag path, matching
516
+ # the behavior of send_message when the version probe returns nil.
517
+ nil
518
+ end
519
+
478
520
  def version_probe_cache_key(env)
479
521
  [
480
522
  probe_env_cache_component(env, "PATH", inherited_label: :inherited_path, override_label: :path_override),
@@ -130,10 +130,16 @@ module AgentHarness
130
130
  end
131
131
 
132
132
  def config_file_content(options = {})
133
- {
134
- provider: options[:api_provider],
135
- model: options[:model_id]
136
- }.to_json
133
+ # Only use explicit provider_name or default to "openai".
134
+ # api_provider is a generic backend label (e.g. "openrouter") that is not
135
+ # a valid Kilo built-in provider ID, so we must not fall back to it here.
136
+ provider_name = options[:provider_name] || "openai"
137
+ model_id = options[:model_id]
138
+
139
+ config = {provider: {provider_name => {}}}
140
+ config[:model] = "#{provider_name}/#{model_id}" if model_id
141
+
142
+ config.to_json
137
143
  end
138
144
 
139
145
  def error_patterns
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.15.0"
4
+ VERSION = "0.16.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.15.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan