agent-harness 0.17.0 → 0.17.2
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 +14 -0
- data/README.md +3 -2
- data/lib/agent_harness/configuration.rb +2 -1
- data/lib/agent_harness/provider_health_check.rb +12 -1
- data/lib/agent_harness/provider_runtime.rb +13 -6
- data/lib/agent_harness/providers/pi.rb +214 -0
- data/lib/agent_harness/providers/registry.rb +1 -0
- 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: f15e1c2315d17c1250d547c58c3f04b2d9daf491a3b56fcb23e58b824a68e6a1
|
|
4
|
+
data.tar.gz: 42bd06fe77703e25a8c61ec8667de90d1b3e84e483d1651b36c0e0013cdc610a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba35bf2c9b5e6de554664cfbf1dacbb70d0d9ceb017c6fc1be8bb8c63b73ab295b1d01e28de3650ac74d08d4908650dd11dfc267099482e2ea933a24ae33d5e9
|
|
7
|
+
data.tar.gz: c8fcf04ce5c2ab05c2007fc6642457adbe2bafb48cc11e0290b97834d269c4fbe8be411d9227aceef8315975d97a9ff38d8beae270d34365f07f2e851c883ddb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.17.2](https://github.com/viamin/agent-harness/compare/agent-harness/v0.17.1...agent-harness/v0.17.2) (2026-05-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* 160: Add support for the pi agent CLI ([#203](https://github.com/viamin/agent-harness/issues/203)) ([0aeb607](https://github.com/viamin/agent-harness/commit/0aeb607ea98b52ba8202726dc946b8c1db09a3cd))
|
|
9
|
+
|
|
10
|
+
## [0.17.1](https://github.com/viamin/agent-harness/compare/agent-harness/v0.17.0...agent-harness/v0.17.1) (2026-05-05)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* 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))
|
|
16
|
+
|
|
3
17
|
## [0.17.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.16.1...agent-harness/v0.17.0) (2026-05-03)
|
|
4
18
|
|
|
5
19
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A unified Ruby interface for CLI-based AI coding agents like Claude Code, Cursor
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Unified Interface**: Single API for multiple AI coding agents
|
|
8
|
-
- **
|
|
8
|
+
- **10 Built-in Providers**: Claude Code, Cursor, Gemini CLI, GitHub Copilot, Codex, Pi, Aider, OpenCode, Kilocode, Mistral Vibe
|
|
9
9
|
- **Full Orchestration**: Provider switching, circuit breakers, rate limiting, and health monitoring
|
|
10
10
|
- **Flexible Configuration**: YAML, Ruby DSL, or environment variables
|
|
11
11
|
- **Token Tracking**: Monitor usage across providers for cost and limit management
|
|
@@ -104,6 +104,7 @@ end
|
|
|
104
104
|
| `:gemini` | `gemini` | Google Gemini CLI |
|
|
105
105
|
| `:github_copilot` | `copilot` | GitHub Copilot CLI |
|
|
106
106
|
| `:codex` | `codex` | OpenAI Codex CLI |
|
|
107
|
+
| `:pi` | `pi` | Pi coding agent CLI |
|
|
107
108
|
| `:aider` | `aider` | Aider coding assistant |
|
|
108
109
|
| `:opencode` | `opencode` | OpenCode CLI |
|
|
109
110
|
| `:kilocode` | `kilo` | Kilocode CLI |
|
|
@@ -187,7 +188,7 @@ puts contract[:supported_versions][:requirement]
|
|
|
187
188
|
|
|
188
189
|
# List all registered providers
|
|
189
190
|
AgentHarness::Providers::Registry.instance.all
|
|
190
|
-
# => [:claude, :cursor, :gemini, :github_copilot, :codex, :opencode, :kilocode, :aider, :mistral_vibe]
|
|
191
|
+
# => [:claude, :cursor, :gemini, :github_copilot, :pi, :codex, :opencode, :kilocode, :aider, :mistral_vibe]
|
|
191
192
|
```
|
|
192
193
|
|
|
193
194
|
For Claude, the install contract is the first-class source of truth for:
|
|
@@ -377,7 +377,7 @@ module AgentHarness
|
|
|
377
377
|
# Provider-specific configuration
|
|
378
378
|
class ProviderConfig
|
|
379
379
|
attr_accessor :enabled, :type, :priority, :models, :default_flags, :timeout, :model,
|
|
380
|
-
:externally_sandboxed
|
|
380
|
+
:provider, :externally_sandboxed
|
|
381
381
|
|
|
382
382
|
attr_reader :name
|
|
383
383
|
|
|
@@ -390,6 +390,7 @@ module AgentHarness
|
|
|
390
390
|
@default_flags = []
|
|
391
391
|
@timeout = nil
|
|
392
392
|
@model = nil
|
|
393
|
+
@provider = nil
|
|
393
394
|
@externally_sandboxed = false
|
|
394
395
|
end
|
|
395
396
|
|
|
@@ -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
|
-
|
|
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(
|
|
@@ -46,9 +46,9 @@ module AgentHarness
|
|
|
46
46
|
validate_optional_string!(:base_url, base_url)
|
|
47
47
|
validate_optional_string!(:api_provider, api_provider)
|
|
48
48
|
|
|
49
|
-
@model = model
|
|
50
|
-
@base_url = base_url
|
|
51
|
-
@api_provider = api_provider
|
|
49
|
+
@model = normalize_optional_string(model)
|
|
50
|
+
@base_url = normalize_optional_string(base_url)
|
|
51
|
+
@api_provider = normalize_optional_string(api_provider)
|
|
52
52
|
|
|
53
53
|
env_hash = env.nil? ? {} : env
|
|
54
54
|
unless env_hash.is_a?(Hash)
|
|
@@ -113,9 +113,9 @@ module AgentHarness
|
|
|
113
113
|
"chat_tools must be an Array of Hashes; invalid element at index #{index}: #{tool.inspect} (#{tool.class})"
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
|
-
@chat_base_url = chat_base_url
|
|
117
|
-
@chat_model = chat_model
|
|
118
|
-
@chat_api_key = chat_api_key
|
|
116
|
+
@chat_base_url = normalize_optional_string(chat_base_url)
|
|
117
|
+
@chat_model = normalize_optional_string(chat_model)
|
|
118
|
+
@chat_api_key = normalize_optional_string(chat_api_key)
|
|
119
119
|
@chat_max_tokens = chat_max_tokens
|
|
120
120
|
@chat_tools = normalized_chat_tools&.freeze
|
|
121
121
|
|
|
@@ -191,5 +191,12 @@ module AgentHarness
|
|
|
191
191
|
|
|
192
192
|
raise ArgumentError, "#{name} must be a String or nil (got #{value.class})"
|
|
193
193
|
end
|
|
194
|
+
|
|
195
|
+
def normalize_optional_string(value)
|
|
196
|
+
return value unless value.respond_to?(:strip)
|
|
197
|
+
|
|
198
|
+
normalized = value.strip
|
|
199
|
+
normalized.empty? ? nil : normalized
|
|
200
|
+
end
|
|
194
201
|
end
|
|
195
202
|
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems/requirement"
|
|
4
|
+
|
|
5
|
+
module AgentHarness
|
|
6
|
+
module Providers
|
|
7
|
+
# Pi coding agent CLI provider
|
|
8
|
+
#
|
|
9
|
+
# Provides integration with the Pi terminal coding agent.
|
|
10
|
+
class Pi < Base
|
|
11
|
+
CLI_PACKAGE = "@mariozechner/pi-coding-agent"
|
|
12
|
+
SUPPORTED_CLI_VERSION = "0.73.0"
|
|
13
|
+
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new("= #{SUPPORTED_CLI_VERSION}").freeze
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def provider_name
|
|
17
|
+
:pi
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def binary_name
|
|
21
|
+
"pi"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def available?
|
|
25
|
+
executor = AgentHarness.configuration.command_executor
|
|
26
|
+
!!executor.which(binary_name)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def provider_metadata_overrides
|
|
30
|
+
{
|
|
31
|
+
auth: {
|
|
32
|
+
service: :pi,
|
|
33
|
+
api_family: :multi_provider
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def firewall_requirements
|
|
39
|
+
{
|
|
40
|
+
domains: [
|
|
41
|
+
"pi.dev"
|
|
42
|
+
],
|
|
43
|
+
ip_ranges: []
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def instruction_file_paths
|
|
48
|
+
[
|
|
49
|
+
{
|
|
50
|
+
path: "AGENTS.md",
|
|
51
|
+
description: "Pi agent instructions",
|
|
52
|
+
symlink: false
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
path: "SYSTEM.md",
|
|
56
|
+
description: "Pi system prompt override",
|
|
57
|
+
symlink: false
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def discover_models
|
|
63
|
+
return [] unless available?
|
|
64
|
+
[]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
68
|
+
version = version.strip if version.respond_to?(:strip)
|
|
69
|
+
|
|
70
|
+
unless version.is_a?(String) && !version.empty?
|
|
71
|
+
raise ArgumentError,
|
|
72
|
+
"Unsupported Pi CLI version #{version.inspect}; " \
|
|
73
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
parsed_version = begin
|
|
77
|
+
Gem::Version.new(version)
|
|
78
|
+
rescue ArgumentError
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"Unsupported Pi CLI version #{version.inspect}; " \
|
|
81
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
unless SUPPORTED_CLI_REQUIREMENT.satisfied_by?(parsed_version)
|
|
85
|
+
raise ArgumentError,
|
|
86
|
+
"Unsupported Pi CLI version #{version.inspect}; " \
|
|
87
|
+
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
package = "#{CLI_PACKAGE}@#{version}".freeze
|
|
91
|
+
install_command_prefix = ["npm", "install", "-g", "--ignore-scripts"].freeze
|
|
92
|
+
install_command = (install_command_prefix + [package]).freeze
|
|
93
|
+
supported_versions = [version].freeze
|
|
94
|
+
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
95
|
+
.map { |op, ver| "#{op} #{ver}".freeze }
|
|
96
|
+
.freeze
|
|
97
|
+
|
|
98
|
+
contract = {
|
|
99
|
+
source: :npm,
|
|
100
|
+
package: package,
|
|
101
|
+
package_name: CLI_PACKAGE,
|
|
102
|
+
version: version,
|
|
103
|
+
version_requirement: version_requirement,
|
|
104
|
+
binary_name: binary_name,
|
|
105
|
+
install_command_prefix: install_command_prefix,
|
|
106
|
+
install_command: install_command,
|
|
107
|
+
supported_versions: supported_versions
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
contract.each_value do |value|
|
|
111
|
+
value.freeze if value.is_a?(String)
|
|
112
|
+
end
|
|
113
|
+
contract.freeze
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def smoke_test_contract
|
|
117
|
+
Base::DEFAULT_SMOKE_TEST_CONTRACT
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def name
|
|
122
|
+
"pi"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def display_name
|
|
126
|
+
"Pi Coding Agent"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def configuration_schema
|
|
130
|
+
{
|
|
131
|
+
fields: [
|
|
132
|
+
{
|
|
133
|
+
name: :model,
|
|
134
|
+
type: :string,
|
|
135
|
+
label: "Model",
|
|
136
|
+
required: false,
|
|
137
|
+
hint: "Pi model pattern or ID passed to --model"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: :provider,
|
|
141
|
+
type: :string,
|
|
142
|
+
label: "Provider",
|
|
143
|
+
required: false,
|
|
144
|
+
hint: "Pi provider name passed to --provider"
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
auth_modes: %i[api_key oauth],
|
|
148
|
+
openai_compatible: false
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def capabilities
|
|
153
|
+
{
|
|
154
|
+
streaming: false,
|
|
155
|
+
file_upload: true,
|
|
156
|
+
vision: true,
|
|
157
|
+
tool_use: true,
|
|
158
|
+
# Pi's non-interactive CLI currently exposes only text print mode.
|
|
159
|
+
# Keep JSON mode disabled until the CLI ships a structured output flag.
|
|
160
|
+
json_mode: false,
|
|
161
|
+
mcp: false,
|
|
162
|
+
dangerous_mode: false
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def error_patterns
|
|
167
|
+
COMMON_ERROR_PATTERNS
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def supports_tool_control?
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def auth_type
|
|
175
|
+
:oauth
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def execution_semantics
|
|
179
|
+
{
|
|
180
|
+
prompt_delivery: :flag,
|
|
181
|
+
output_format: :text,
|
|
182
|
+
sandbox_aware: false,
|
|
183
|
+
uses_subcommand: false,
|
|
184
|
+
non_interactive_flag: "-p",
|
|
185
|
+
legitimate_exit_codes: [0],
|
|
186
|
+
stderr_is_diagnostic: true,
|
|
187
|
+
parses_rate_limit_reset: false
|
|
188
|
+
}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
protected
|
|
192
|
+
|
|
193
|
+
def build_command(prompt, options)
|
|
194
|
+
runtime = options[:provider_runtime]
|
|
195
|
+
provider = runtime&.api_provider || @config.provider
|
|
196
|
+
model = runtime&.model || @config.model
|
|
197
|
+
|
|
198
|
+
cmd = [self.class.binary_name, "--no-session"]
|
|
199
|
+
cmd += @config.default_flags if @config.default_flags&.any?
|
|
200
|
+
cmd += runtime.flags if runtime
|
|
201
|
+
cmd += ["--provider", provider] if provider
|
|
202
|
+
cmd += ["--model", model] if model
|
|
203
|
+
cmd << "--no-tools" if options[:tools] == :none
|
|
204
|
+
cmd += ["-p", prompt]
|
|
205
|
+
|
|
206
|
+
cmd
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def default_timeout
|
|
210
|
+
300
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -27,6 +27,7 @@ module AgentHarness
|
|
|
27
27
|
class_name: :GithubCopilot,
|
|
28
28
|
aliases: [:copilot]
|
|
29
29
|
},
|
|
30
|
+
{name: :pi, require_path: "agent_harness/providers/pi", class_name: :Pi, aliases: []},
|
|
30
31
|
{name: :codex, require_path: "agent_harness/providers/codex", class_name: :Codex, aliases: []},
|
|
31
32
|
{name: :opencode, require_path: "agent_harness/providers/opencode", class_name: :Opencode, aliases: []},
|
|
32
33
|
{name: :kilocode, require_path: "agent_harness/providers/kilocode", class_name: :Kilocode, aliases: []},
|
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.17.
|
|
4
|
+
version: 0.17.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -133,6 +133,7 @@ files:
|
|
|
133
133
|
- lib/agent_harness/providers/mcp_config_file_support.rb
|
|
134
134
|
- lib/agent_harness/providers/mistral_vibe.rb
|
|
135
135
|
- lib/agent_harness/providers/opencode.rb
|
|
136
|
+
- lib/agent_harness/providers/pi.rb
|
|
136
137
|
- lib/agent_harness/providers/rate_limit_reset_parsing.rb
|
|
137
138
|
- lib/agent_harness/providers/registry.rb
|
|
138
139
|
- lib/agent_harness/providers/token_usage_parsing.rb
|