agent-harness 0.21.0 → 0.22.0
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 +7 -0
- data/lib/agent_harness/dependency_updater.rb +130 -0
- data/lib/agent_harness/release_registry.rb +86 -0
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +27 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e913f16d4377e9cc016dc2cee6018201358532fe61af764a5d860ef94e67711
|
|
4
|
+
data.tar.gz: 2b3ab503acfc2cc7493b33f8dfb7d5969e01e9980b0eb08181c2eb420b7dcecb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae9b5863d6d7edb3036abdb9217996f66479cf3fec35473aa16443879b11d096ccc9869920264ecf39255245c65fc062c5bcfd671e153ae679d50c7ebe4e85d1
|
|
7
|
+
data.tar.gz: 157f5c0ebad4cc73a7a1d8224506de837a0b84ef5a66d4c8795131340eb6e2a40ca6f5f8a9293c1a4fa7625bbeda9cad52e38f294ec74633ccbfb6e2ce1274b6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.22.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.21.0...agent-harness/v0.22.0) (2026-06-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* automated dependency updates for installable agents with cooldown period ([#239](https://github.com/viamin/agent-harness/issues/239)) ([0682cc0](https://github.com/viamin/agent-harness/commit/0682cc0d40264f5b5431fc0a7ab0c0d76416ec64))
|
|
9
|
+
|
|
3
10
|
## [0.21.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.20.1...agent-harness/v0.21.0) (2026-06-09)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
class DependencyUpdater
|
|
5
|
+
DEFAULT_COOLDOWN_SECONDS = 3 * 24 * 60 * 60
|
|
6
|
+
|
|
7
|
+
attr_reader :release_registry, :cooldown_period
|
|
8
|
+
|
|
9
|
+
def initialize(cooldown_period: DEFAULT_COOLDOWN_SECONDS, release_registry: nil)
|
|
10
|
+
@cooldown_period = validate_cooldown(cooldown_period)
|
|
11
|
+
@per_provider_cooldown = {}
|
|
12
|
+
@release_registry = release_registry || ReleaseRegistry.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cooldown_period=(value)
|
|
16
|
+
@cooldown_period = validate_cooldown(value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def set_cooldown(provider_name, period)
|
|
20
|
+
provider_name = provider_name.to_sym
|
|
21
|
+
@per_provider_cooldown[provider_name] = validate_cooldown(period)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def clear_cooldown(provider_name)
|
|
25
|
+
@per_provider_cooldown.delete(provider_name.to_sym)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def cooldown_for(provider_name)
|
|
29
|
+
@per_provider_cooldown[provider_name.to_sym] || @cooldown_period
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def register_release(provider_name, version, released_at: Time.now)
|
|
33
|
+
@release_registry.register(provider_name, version, released_at: released_at)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_latest_version(provider_name, bypass_cooldown: false, now: Time.now)
|
|
37
|
+
provider_name = provider_name.to_sym
|
|
38
|
+
available = @release_registry.versions_for(provider_name)
|
|
39
|
+
return nil if available.nil? || available.empty?
|
|
40
|
+
|
|
41
|
+
if bypass_cooldown
|
|
42
|
+
return newest_version(available)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
cooldown = cooldown_for(provider_name)
|
|
46
|
+
eligible = available.select do |entry|
|
|
47
|
+
entry[:released_at].nil? || (now - entry[:released_at]) >= cooldown
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
return nil if eligible.empty?
|
|
51
|
+
|
|
52
|
+
newest_version(eligible)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def eligible?(provider_name, version, bypass_cooldown: false, now: Time.now)
|
|
56
|
+
return true if bypass_cooldown
|
|
57
|
+
|
|
58
|
+
provider_name = provider_name.to_sym
|
|
59
|
+
version = version.to_s
|
|
60
|
+
|
|
61
|
+
released_at = @release_registry.released_at(provider_name, version)
|
|
62
|
+
return true if released_at.nil?
|
|
63
|
+
|
|
64
|
+
cooldown = cooldown_for(provider_name)
|
|
65
|
+
(now - released_at) >= cooldown
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def resolve_latest_installation_contract(provider_name, bypass_cooldown: false, now: Time.now)
|
|
69
|
+
provider_name = provider_name.to_sym
|
|
70
|
+
version_info = resolve_latest_version(provider_name, bypass_cooldown: bypass_cooldown, now: now)
|
|
71
|
+
return nil unless version_info
|
|
72
|
+
|
|
73
|
+
version = version_info[:version]
|
|
74
|
+
begin
|
|
75
|
+
contract = Providers::Registry.instance.installation_contract(provider_name, version: version)
|
|
76
|
+
{
|
|
77
|
+
provider: provider_name,
|
|
78
|
+
version: version,
|
|
79
|
+
released_at: version_info[:released_at],
|
|
80
|
+
installation_contract: contract
|
|
81
|
+
}
|
|
82
|
+
rescue ConfigurationError
|
|
83
|
+
{
|
|
84
|
+
provider: provider_name,
|
|
85
|
+
version: version,
|
|
86
|
+
released_at: version_info[:released_at],
|
|
87
|
+
installation_contract: nil
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_all_latest(bypass_cooldown: false, now: Time.now)
|
|
93
|
+
@release_registry.providers.each_with_object({}) do |provider_name, results|
|
|
94
|
+
version_info = resolve_latest_version(provider_name, bypass_cooldown: bypass_cooldown, now: now)
|
|
95
|
+
results[provider_name] = version_info if version_info
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def validate_cooldown(value)
|
|
102
|
+
numeric = case value
|
|
103
|
+
when Integer
|
|
104
|
+
value
|
|
105
|
+
when Float
|
|
106
|
+
value
|
|
107
|
+
when Numeric
|
|
108
|
+
value
|
|
109
|
+
else
|
|
110
|
+
raise ArgumentError, "cooldown period must be a positive number of seconds, got #{value.inspect}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
raise ArgumentError, "cooldown period must be positive, got #{numeric}" unless numeric > 0
|
|
114
|
+
|
|
115
|
+
numeric
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def newest_version(entries)
|
|
119
|
+
entries.max_by do |entry|
|
|
120
|
+
parse_version(entry[:version])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_version(version_string)
|
|
125
|
+
Gem::Version.new(version_string)
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
Gem::Version.new("0")
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgentHarness
|
|
4
|
+
class ReleaseRegistry
|
|
5
|
+
attr_reader :releases
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@releases = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register(provider_name, version, released_at: Time.now)
|
|
12
|
+
provider_name = provider_name.to_sym
|
|
13
|
+
version = version.to_s
|
|
14
|
+
|
|
15
|
+
raise ArgumentError, "version must be a non-empty string" if version.empty?
|
|
16
|
+
raise ArgumentError, "released_at must be a Time" unless released_at.is_a?(Time)
|
|
17
|
+
|
|
18
|
+
@releases[provider_name] ||= []
|
|
19
|
+
entry = {version: version, released_at: released_at}
|
|
20
|
+
existing = @releases[provider_name].find { |e| e[:version] == version }
|
|
21
|
+
if existing
|
|
22
|
+
existing[:released_at] = released_at
|
|
23
|
+
else
|
|
24
|
+
@releases[provider_name] << entry
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
entry
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def versions_for(provider_name)
|
|
31
|
+
provider_name = provider_name.to_sym
|
|
32
|
+
return nil unless @releases.key?(provider_name)
|
|
33
|
+
|
|
34
|
+
@releases[provider_name].dup
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def released_at(provider_name, version)
|
|
38
|
+
provider_name = provider_name.to_sym
|
|
39
|
+
version = version.to_s
|
|
40
|
+
|
|
41
|
+
entries = @releases[provider_name]
|
|
42
|
+
return nil unless entries
|
|
43
|
+
|
|
44
|
+
entry = entries.find { |e| e[:version] == version }
|
|
45
|
+
entry&.fetch(:released_at, nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def registered?(provider_name, version)
|
|
49
|
+
provider_name = provider_name.to_sym
|
|
50
|
+
version = version.to_s
|
|
51
|
+
|
|
52
|
+
entries = @releases[provider_name]
|
|
53
|
+
return false unless entries
|
|
54
|
+
|
|
55
|
+
entries.any? { |e| e[:version] == version }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def providers
|
|
59
|
+
@releases.keys
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear
|
|
63
|
+
@releases.clear
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear_provider(provider_name)
|
|
67
|
+
@releases.delete(provider_name.to_sym)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def merge!(other_registry)
|
|
71
|
+
other_registry.releases.each do |provider_name, entries|
|
|
72
|
+
@releases[provider_name] ||= []
|
|
73
|
+
entries.each do |entry|
|
|
74
|
+
existing = @releases[provider_name].find { |e| e[:version] == entry[:version] }
|
|
75
|
+
if existing
|
|
76
|
+
existing[:released_at] = entry[:released_at]
|
|
77
|
+
else
|
|
78
|
+
@releases[provider_name] << entry.dup
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/agent_harness.rb
CHANGED
|
@@ -47,6 +47,7 @@ module AgentHarness
|
|
|
47
47
|
@configuration = nil
|
|
48
48
|
@conductor = nil
|
|
49
49
|
@token_tracker = nil
|
|
50
|
+
@dependency_updater = nil
|
|
50
51
|
Skills.reset! if defined?(Skills)
|
|
51
52
|
end
|
|
52
53
|
|
|
@@ -341,6 +342,30 @@ module AgentHarness
|
|
|
341
342
|
options[:provider_runtime] = provider_runtime unless provider_runtime.nil?
|
|
342
343
|
ProviderHealthCheck.check(provider_name, **options)
|
|
343
344
|
end
|
|
345
|
+
|
|
346
|
+
# Returns the global dependency updater for managing agent tool versions.
|
|
347
|
+
#
|
|
348
|
+
# The dependency updater applies a configurable cooldown period before
|
|
349
|
+
# adopting new upstream releases, reducing exposure to regressions.
|
|
350
|
+
#
|
|
351
|
+
# @return [DependencyUpdater] the dependency updater instance
|
|
352
|
+
def dependency_updater
|
|
353
|
+
@dependency_updater ||= DependencyUpdater.new
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Resolve the latest eligible version for an installable provider tool,
|
|
357
|
+
# applying the configured cooldown period.
|
|
358
|
+
#
|
|
359
|
+
# @param provider_name [Symbol, String] the provider name
|
|
360
|
+
# @param bypass_cooldown [Boolean] when true, skip the cooldown check
|
|
361
|
+
# @return [Hash, nil] version info with :provider, :version, :released_at,
|
|
362
|
+
# :installation_contract keys, or nil when no eligible version exists
|
|
363
|
+
def resolve_latest_version(provider_name, bypass_cooldown: false)
|
|
364
|
+
dependency_updater.resolve_latest_installation_contract(
|
|
365
|
+
provider_name,
|
|
366
|
+
bypass_cooldown: bypass_cooldown
|
|
367
|
+
)
|
|
368
|
+
end
|
|
344
369
|
end
|
|
345
370
|
end
|
|
346
371
|
|
|
@@ -368,6 +393,8 @@ require_relative "agent_harness/openai_compatible_transport"
|
|
|
368
393
|
require_relative "agent_harness/conversation"
|
|
369
394
|
require_relative "agent_harness/authentication"
|
|
370
395
|
require_relative "agent_harness/provider_health_check"
|
|
396
|
+
require_relative "agent_harness/release_registry"
|
|
397
|
+
require_relative "agent_harness/dependency_updater"
|
|
371
398
|
|
|
372
399
|
# Provider layer
|
|
373
400
|
require_relative "agent_harness/providers/registry"
|
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.
|
|
4
|
+
version: 0.22.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -104,6 +104,7 @@ files:
|
|
|
104
104
|
- lib/agent_harness/command_executor.rb
|
|
105
105
|
- lib/agent_harness/configuration.rb
|
|
106
106
|
- lib/agent_harness/conversation.rb
|
|
107
|
+
- lib/agent_harness/dependency_updater.rb
|
|
107
108
|
- lib/agent_harness/docker_command_executor.rb
|
|
108
109
|
- lib/agent_harness/error_taxonomy.rb
|
|
109
110
|
- lib/agent_harness/errors.rb
|
|
@@ -137,6 +138,7 @@ files:
|
|
|
137
138
|
- lib/agent_harness/providers/rate_limit_reset_parsing.rb
|
|
138
139
|
- lib/agent_harness/providers/registry.rb
|
|
139
140
|
- lib/agent_harness/providers/token_usage_parsing.rb
|
|
141
|
+
- lib/agent_harness/release_registry.rb
|
|
140
142
|
- lib/agent_harness/response.rb
|
|
141
143
|
- lib/agent_harness/skill.rb
|
|
142
144
|
- lib/agent_harness/skills.rb
|