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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c94304f4eef10eed2b3a21cd345ff55a6aceb28c4cb86a4415b293ed29f7c7f2
4
- data.tar.gz: 1bc568febc4ef6a45d4e9271eb50554e2a49662f2fc0c4ad3a2db876a8eeedbe
3
+ metadata.gz: 1e913f16d4377e9cc016dc2cee6018201358532fe61af764a5d860ef94e67711
4
+ data.tar.gz: 2b3ab503acfc2cc7493b33f8dfb7d5969e01e9980b0eb08181c2eb420b7dcecb
5
5
  SHA512:
6
- metadata.gz: f9b375833a4541a9c364385432a9b3f7d279d0c09258696773ba6a3d333a871bdb10299f8d0e5a17989fdd787f786f7eb2477c589cc4dd72bd48df456ee6231e
7
- data.tar.gz: d1cd7d45cf20094fa551d1dcc4e988c01e9e11daa9d0c00806fc4ec19e56ea9c06189d6fdf1214a98ec92a2dae23fcad538e5a31ca22d3e154c275bf7bedf3de
6
+ metadata.gz: ae9b5863d6d7edb3036abdb9217996f66479cf3fec35473aa16443879b11d096ccc9869920264ecf39255245c65fc062c5bcfd671e153ae679d50c7ebe4e85d1
7
+ data.tar.gz: 157f5c0ebad4cc73a7a1d8224506de837a0b84ef5a66d4c8795131340eb6e2a40ca6f5f8a9293c1a4fa7625bbeda9cad52e38f294ec74633ccbfb6e2ce1274b6
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.21.0"
2
+ ".": "0.22.0"
3
3
  }
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.21.0"
4
+ VERSION = "0.22.0"
5
5
  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.21.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