ace-llm 0.41.1 → 0.42.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/CHANGELOG.md +9 -0
- data/lib/ace/llm/atoms/error_classifier.rb +36 -1
- data/lib/ace/llm/models/execution_evidence.rb +182 -0
- data/lib/ace/llm/molecules/fallback_orchestrator.rb +44 -10
- data/lib/ace/llm/version.rb +1 -1
- data/lib/ace/llm.rb +10 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b93f7041470f94d60776c4d9659cd403909a02777f3e8f6453e040cb62d5ae7
|
|
4
|
+
data.tar.gz: 812993967a5d36cedfccc9a1789303ca8abd9882fb5b53d7d2eda7413e82488f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 497b2a465fd61863b06b4b4a4da6f5c1919bdf3a6228e1964994a0204fa0bcdf7c78870f1717f43ba485c0cfd4a1d4ca5d72430c4add742853c95b69c176dfa4
|
|
7
|
+
data.tar.gz: a1fc3830077342a69ad444ff03f80e88f4ac899d4a2dc9f71b485f33262c772790fd46231ecba3fd82e2bc47a4445f779fd429df25b7247ce1f3e8adc88952fd
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.42.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `Ace::LLM::Models::ExecutionEvidence`: structured, immutable record of how a CLI provider execution actually ended (outcome kind, invocation correlation ID, configured deadline, monotonic elapsed time, exit status/signal, bounded output excerpts). `Ace::LLM::ProviderError` instances can now carry it via `execution_evidence`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `ErrorClassifier` prefers structured execution evidence over error-message prose and returns the new `EXECUTION_INCOMPLETE` classification when a provider session began and then ended without a confirmed completion (deadline expiry, transport drop, nonzero exit, or clean exit without a final response). A nonzero exit whose stderr merely mentions "timeout" is no longer classified as a timeout; a successful response containing the word is unaffected.
|
|
17
|
+
- `FallbackOrchestrator` aborts the whole chain (no retry, no fallback) on execution-incomplete failures — the provider process already ran, so replaying via another provider would repeat its side effects. The terminal error carries the evidence summary, the provider's own bounded diagnostic line, and the list of tried providers; all-providers-exhausted messages include the last execution evidence.
|
|
18
|
+
|
|
10
19
|
## [0.41.1] - 2026-09-29
|
|
11
20
|
|
|
12
21
|
### Changed
|
|
@@ -11,6 +11,11 @@ module Ace
|
|
|
11
11
|
FALLBACK_IMMEDIATELY = :fallback_immediately
|
|
12
12
|
SKIP_TO_NEXT = :skip_to_next
|
|
13
13
|
TERMINAL = :terminal
|
|
14
|
+
# The provider process started but its session ended without a confirmed
|
|
15
|
+
# completion (deadline expiry, transport drop, nonzero exit, or a clean
|
|
16
|
+
# exit without a final response). Such sessions may have performed side
|
|
17
|
+
# effects — retries and fallback would replay them.
|
|
18
|
+
EXECUTION_INCOMPLETE = :execution_incomplete
|
|
14
19
|
|
|
15
20
|
# Map HTTP status codes to classification
|
|
16
21
|
STATUS_CLASSIFICATIONS = {
|
|
@@ -43,8 +48,13 @@ module Ace
|
|
|
43
48
|
|
|
44
49
|
# Classify an error for retry/fallback decisions
|
|
45
50
|
# @param error [Exception] The error to classify
|
|
46
|
-
# @return [Symbol] Classification type (RETRYABLE_WITH_BACKOFF, FALLBACK_IMMEDIATELY, SKIP_TO_NEXT, TERMINAL)
|
|
51
|
+
# @return [Symbol] Classification type (RETRYABLE_WITH_BACKOFF, FALLBACK_IMMEDIATELY, SKIP_TO_NEXT, TERMINAL, EXECUTION_INCOMPLETE)
|
|
47
52
|
def self.classify(error)
|
|
53
|
+
# Structured capture evidence attached at the subprocess boundary
|
|
54
|
+
# outranks message prose: a nonzero exit whose stderr mentions
|
|
55
|
+
# "timeout" is a nonzero exit, not a deadline expiry.
|
|
56
|
+
return EXECUTION_INCOMPLETE if execution_incomplete?(error)
|
|
57
|
+
|
|
48
58
|
case error
|
|
49
59
|
when Ace::LLM::AuthenticationError
|
|
50
60
|
SKIP_TO_NEXT
|
|
@@ -64,6 +74,31 @@ module Ace
|
|
|
64
74
|
end
|
|
65
75
|
end
|
|
66
76
|
|
|
77
|
+
# True when the error carries structured evidence that the provider
|
|
78
|
+
# execution began and ended without a confirmed completion.
|
|
79
|
+
# @param error [Exception]
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def self.execution_incomplete?(error)
|
|
82
|
+
evidence = execution_evidence_for(error)
|
|
83
|
+
evidence&.uncertain_execution? ? true : false
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Extract structured execution evidence from an error, accepting either
|
|
87
|
+
# an ExecutionEvidence instance or its serialized hash form.
|
|
88
|
+
# @param error [Exception]
|
|
89
|
+
# @return [Ace::LLM::Models::ExecutionEvidence, nil]
|
|
90
|
+
def self.execution_evidence_for(error)
|
|
91
|
+
return nil unless error.respond_to?(:execution_evidence)
|
|
92
|
+
|
|
93
|
+
evidence = error.execution_evidence
|
|
94
|
+
case evidence
|
|
95
|
+
when Ace::LLM::Models::ExecutionEvidence
|
|
96
|
+
evidence
|
|
97
|
+
when Hash
|
|
98
|
+
Ace::LLM::Models::ExecutionEvidence.from_h(evidence)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
67
102
|
# Determine if an error is retryable
|
|
68
103
|
# @param error [Exception] The error to check
|
|
69
104
|
# @return [Boolean] True if error should be retried
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module LLM
|
|
5
|
+
module Models
|
|
6
|
+
# Structured evidence about how a CLI provider execution actually ended.
|
|
7
|
+
#
|
|
8
|
+
# Built at the subprocess boundary (ace-llm-providers-cli SafeCapture) and
|
|
9
|
+
# attached to Ace::LLM::ProviderError instances so classification and
|
|
10
|
+
# fallback decisions can key on observed process facts instead of error
|
|
11
|
+
# message prose. Carries bounded output excerpts only — never the full
|
|
12
|
+
# streams, prompt content, or environment.
|
|
13
|
+
#
|
|
14
|
+
# This is a model - pure immutable data.
|
|
15
|
+
class ExecutionEvidence
|
|
16
|
+
OUTCOMES = %i[
|
|
17
|
+
deadline_exceeded
|
|
18
|
+
transport_failure
|
|
19
|
+
nonzero_exit
|
|
20
|
+
no_response
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
EXCERPT_LIMIT = 2000
|
|
24
|
+
|
|
25
|
+
# Credential shapes that must not survive into diagnostics, reports, or
|
|
26
|
+
# error messages. Matches are truncated to a short prefix so the
|
|
27
|
+
# credential TYPE stays readable while the value is destroyed.
|
|
28
|
+
SECRET_PATTERNS = [
|
|
29
|
+
/(?:bearer|authorization|basic)\s*[:=]?\s*[\w.\-+=\/]{8,}/i,
|
|
30
|
+
/(?:api[_-]?key|access[_-]?token|auth[_-]?token|refresh[_-]?token|client[_-]?secret|secret|password|passwd|pwd)\s*["']?\s*[:=]\s*["']?[\w.\-+=\/]{8,}/i,
|
|
31
|
+
/sk-(?:proj-)?[A-Za-z0-9_\-]{16,}/,
|
|
32
|
+
/gh[pousr]_[A-Za-z0-9_]{16,}/,
|
|
33
|
+
/AKIA[0-9A-Z]{16}/,
|
|
34
|
+
/xox[baprs]-[A-Za-z0-9\-]{10,}/,
|
|
35
|
+
/eyJ[A-Za-z0-9_\-]{20,}\.eyJ[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{10,}/
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
# Destroy credential values in captured diagnostics while keeping the
|
|
39
|
+
# surrounding text readable.
|
|
40
|
+
# @param text [String, nil]
|
|
41
|
+
# @return [String, nil]
|
|
42
|
+
def self.redact(text)
|
|
43
|
+
return text if text.nil?
|
|
44
|
+
|
|
45
|
+
redacted = text.to_s.dup
|
|
46
|
+
SECRET_PATTERNS.each do |pattern|
|
|
47
|
+
redacted = redacted.gsub(pattern) { |match| "#{match[0, 6]}…[redacted]" }
|
|
48
|
+
end
|
|
49
|
+
redacted
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
attr_reader :outcome, :invocation_id, :provider_name, :deadline_seconds,
|
|
53
|
+
:elapsed_seconds, :exit_status, :signal, :execution_began,
|
|
54
|
+
:stdout_excerpt, :stderr_excerpt
|
|
55
|
+
|
|
56
|
+
# @param outcome [Symbol] one of OUTCOMES
|
|
57
|
+
# @param invocation_id [String, nil] opaque correlation ID generated before spawn
|
|
58
|
+
# @param provider_name [String, nil] human-facing provider label (e.g. "Codex")
|
|
59
|
+
# @param deadline_seconds [Numeric, nil] configured deadline for the invocation
|
|
60
|
+
# @param elapsed_seconds [Numeric, nil] monotonic elapsed time of the invocation
|
|
61
|
+
# @param exit_status [Integer, nil] process exit code when the process exited
|
|
62
|
+
# @param signal [String, nil] signal name when the process was terminated by one
|
|
63
|
+
# @param execution_began [Boolean] true when the provider process was actually spawned
|
|
64
|
+
# @param stdout_excerpt [String, nil] bounded excerpt of captured stdout
|
|
65
|
+
# @param stderr_excerpt [String, nil] bounded excerpt of captured stderr
|
|
66
|
+
def initialize(outcome:, invocation_id: nil, provider_name: nil, deadline_seconds: nil,
|
|
67
|
+
elapsed_seconds: nil, exit_status: nil, signal: nil, execution_began: true,
|
|
68
|
+
stdout_excerpt: nil, stderr_excerpt: nil)
|
|
69
|
+
raise ArgumentError, "unknown outcome #{outcome.inspect}" unless OUTCOMES.include?(outcome)
|
|
70
|
+
|
|
71
|
+
@outcome = outcome
|
|
72
|
+
@invocation_id = invocation_id
|
|
73
|
+
@provider_name = provider_name
|
|
74
|
+
@deadline_seconds = deadline_seconds
|
|
75
|
+
@elapsed_seconds = elapsed_seconds
|
|
76
|
+
@exit_status = exit_status
|
|
77
|
+
@signal = signal
|
|
78
|
+
@execution_began = execution_began
|
|
79
|
+
@stdout_excerpt = bounded_excerpt(stdout_excerpt)
|
|
80
|
+
@stderr_excerpt = bounded_excerpt(stderr_excerpt)
|
|
81
|
+
freeze
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Build evidence from a plain hash (e.g. deserialized metadata)
|
|
85
|
+
# @param hash [Hash]
|
|
86
|
+
# @return [ExecutionEvidence]
|
|
87
|
+
def self.from_h(hash)
|
|
88
|
+
return nil unless hash.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
symbolized = hash.each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value }
|
|
91
|
+
outcome = symbolized[:outcome].to_s.to_sym
|
|
92
|
+
return nil unless OUTCOMES.include?(outcome)
|
|
93
|
+
|
|
94
|
+
new(
|
|
95
|
+
outcome: outcome,
|
|
96
|
+
invocation_id: symbolized[:invocation_id],
|
|
97
|
+
provider_name: symbolized[:provider_name],
|
|
98
|
+
deadline_seconds: symbolized[:deadline_seconds],
|
|
99
|
+
elapsed_seconds: symbolized[:elapsed_seconds],
|
|
100
|
+
exit_status: symbolized[:exit_status],
|
|
101
|
+
signal: symbolized[:signal],
|
|
102
|
+
execution_began: symbolized.fetch(:execution_began, true),
|
|
103
|
+
stdout_excerpt: symbolized[:stdout_excerpt],
|
|
104
|
+
stderr_excerpt: symbolized[:stderr_excerpt]
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# True when the provider process was spawned and its final state is not
|
|
109
|
+
# a confirmed successful completion. Such sessions may have performed
|
|
110
|
+
# side effects; callers must not replay them automatically.
|
|
111
|
+
def uncertain_execution?
|
|
112
|
+
execution_began
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# First bounded diagnostic line from the captured streams, for
|
|
116
|
+
# actionable terminal messages. Prefers stderr; falls back to stdout.
|
|
117
|
+
# @return [String, nil]
|
|
118
|
+
def detail_line
|
|
119
|
+
source = [stderr_excerpt, stdout_excerpt].compact
|
|
120
|
+
.map { |stream| stream.lines.map(&:strip) }
|
|
121
|
+
.flatten
|
|
122
|
+
.reject(&:empty?)
|
|
123
|
+
.first.to_s
|
|
124
|
+
return nil if source.empty?
|
|
125
|
+
|
|
126
|
+
source.length > 300 ? "#{source[0, 300]}…[truncated]" : source
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Human-readable one-line summary safe for reports and error messages.
|
|
130
|
+
# @return [String]
|
|
131
|
+
def summary
|
|
132
|
+
parts = [outcome_label]
|
|
133
|
+
parts << "elapsed #{format_seconds(elapsed_seconds)}" if elapsed_seconds
|
|
134
|
+
parts << "deadline #{format_seconds(deadline_seconds)}" if deadline_seconds
|
|
135
|
+
parts << "exit #{exit_status}" unless exit_status.nil?
|
|
136
|
+
parts << "signal #{signal}" if signal
|
|
137
|
+
parts << "invocation #{invocation_id}" if invocation_id
|
|
138
|
+
parts.join(", ")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Hash] plain hash without output excerpts (for metadata surfaces)
|
|
142
|
+
def to_h
|
|
143
|
+
{
|
|
144
|
+
outcome: outcome,
|
|
145
|
+
invocation_id: invocation_id,
|
|
146
|
+
provider_name: provider_name,
|
|
147
|
+
deadline_seconds: deadline_seconds,
|
|
148
|
+
elapsed_seconds: elapsed_seconds,
|
|
149
|
+
exit_status: exit_status,
|
|
150
|
+
signal: signal,
|
|
151
|
+
execution_began: execution_began
|
|
152
|
+
}.compact
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
def outcome_label
|
|
158
|
+
case outcome
|
|
159
|
+
when :deadline_exceeded then "deadline_exceeded"
|
|
160
|
+
when :transport_failure then "transport_failure"
|
|
161
|
+
when :nonzero_exit then "nonzero_exit"
|
|
162
|
+
when :no_response then "completed_without_final_response"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def format_seconds(value)
|
|
167
|
+
return value.to_s if value.is_a?(String)
|
|
168
|
+
|
|
169
|
+
format("%.1fs", value)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def bounded_excerpt(text)
|
|
173
|
+
return nil if text.nil?
|
|
174
|
+
|
|
175
|
+
excerpt = self.class.redact(text.to_s)
|
|
176
|
+
excerpt = "#{excerpt[0, EXCERPT_LIMIT]}…[truncated]" if excerpt.length > EXCERPT_LIMIT
|
|
177
|
+
excerpt.freeze
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
@@ -22,6 +22,8 @@ module Ace
|
|
|
22
22
|
@visited_providers = Set.new
|
|
23
23
|
@start_time = nil
|
|
24
24
|
@last_failure_terminal = false
|
|
25
|
+
@last_execution_evidence = nil
|
|
26
|
+
@chain_aborted = false
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
# Execute a block with fallback support
|
|
@@ -35,6 +37,7 @@ module Ace
|
|
|
35
37
|
@start_time = Time.now
|
|
36
38
|
@primary_provider = primary_provider
|
|
37
39
|
@visited_providers.clear
|
|
40
|
+
@chain_aborted = false
|
|
38
41
|
|
|
39
42
|
# If fallback disabled, just execute with primary
|
|
40
43
|
return yield(get_client(primary_provider, registry), primary_provider) if @config.disabled?
|
|
@@ -43,6 +46,7 @@ module Ace
|
|
|
43
46
|
result = try_provider_with_retry(primary_provider, registry) do |client, provider_name|
|
|
44
47
|
yield client, provider_name
|
|
45
48
|
end
|
|
49
|
+
raise_incomplete_execution_error if @chain_aborted
|
|
46
50
|
return result if result
|
|
47
51
|
|
|
48
52
|
# Try fallback providers in order (per-provider chain or default)
|
|
@@ -61,6 +65,7 @@ module Ace
|
|
|
61
65
|
result = try_provider_with_retry(fallback_provider, registry) do |client, provider_name|
|
|
62
66
|
yield client, provider_name
|
|
63
67
|
end
|
|
68
|
+
raise_incomplete_execution_error if @chain_aborted
|
|
64
69
|
return result if result
|
|
65
70
|
end
|
|
66
71
|
|
|
@@ -88,27 +93,53 @@ module Ace
|
|
|
88
93
|
raise if error.is_a?(Ace::LLM::ConfigurationError) && provider_name != @primary_provider
|
|
89
94
|
last_error = error
|
|
90
95
|
|
|
91
|
-
# Handle the error - returns :retry or :
|
|
96
|
+
# Handle the error - returns :retry, :stop_and_fallback, or :abort_chain
|
|
92
97
|
action = handle_error(error, provider_name, attempts)
|
|
93
98
|
|
|
94
99
|
if action == :retry
|
|
95
100
|
attempts += 1
|
|
96
101
|
next
|
|
97
|
-
else # :stop_and_fallback
|
|
102
|
+
else # :stop_and_fallback or :abort_chain
|
|
98
103
|
return nil
|
|
99
104
|
end
|
|
100
105
|
end
|
|
101
106
|
end
|
|
102
107
|
|
|
108
|
+
# Raise the terminal error for a chain aborted by an incomplete
|
|
109
|
+
# execution. Carries the structured evidence so downstream consumers
|
|
110
|
+
# (E2E runner, reviewers) can reconcile the session without guessing.
|
|
111
|
+
def raise_incomplete_execution_error
|
|
112
|
+
evidence = @last_execution_evidence
|
|
113
|
+
message = +"Provider execution incomplete; remaining providers were not tried to avoid replaying CLI side effects."
|
|
114
|
+
message << " Evidence: #{evidence.summary}." if evidence
|
|
115
|
+
if evidence && (detail = evidence.detail_line)
|
|
116
|
+
message << " Detail: #{detail}"
|
|
117
|
+
end
|
|
118
|
+
message << " Tried: #{@visited_providers.to_a.join(", ")}."
|
|
119
|
+
message << " Reconcile the session manually before rerunning this work."
|
|
120
|
+
error = Ace::LLM::ProviderError.new(message)
|
|
121
|
+
error.execution_evidence = evidence if evidence
|
|
122
|
+
raise error
|
|
123
|
+
end
|
|
124
|
+
|
|
103
125
|
# Handle error and determine retry/fallback strategy
|
|
104
126
|
# @param error [Exception] The error that occurred
|
|
105
127
|
# @param provider_name [String] Provider name
|
|
106
128
|
# @param attempts [Integer] Current attempt number
|
|
107
|
-
# @return [Symbol] :retry to retry, :stop_and_fallback to move to next provider
|
|
129
|
+
# @return [Symbol] :retry to retry, :stop_and_fallback to move to next provider, :abort_chain to stop entirely
|
|
108
130
|
def handle_error(error, provider_name, attempts)
|
|
109
131
|
classification = Atoms::ErrorClassifier.classify(error)
|
|
110
132
|
|
|
111
133
|
case classification
|
|
134
|
+
when Atoms::ErrorClassifier::EXECUTION_INCOMPLETE
|
|
135
|
+
# The provider process already ran; its session ended without a
|
|
136
|
+
# confirmed completion. Retrying or falling back would replay
|
|
137
|
+
# whatever side effects the session performed.
|
|
138
|
+
@last_failure_terminal = true
|
|
139
|
+
@last_execution_evidence = Atoms::ErrorClassifier.execution_evidence_for(error)
|
|
140
|
+
@chain_aborted = true
|
|
141
|
+
report_status("⚠ #{provider_name} execution incomplete (#{@last_execution_evidence&.summary}); not retrying or falling back to avoid replaying side effects")
|
|
142
|
+
:abort_chain
|
|
112
143
|
when Atoms::ErrorClassifier::SKIP_TO_NEXT
|
|
113
144
|
@last_failure_terminal = false
|
|
114
145
|
report_status("⚠ #{provider_name} authentication failed, skipping...")
|
|
@@ -196,13 +227,16 @@ module Ace
|
|
|
196
227
|
def build_exhaustion_error_message
|
|
197
228
|
providers_tried = @visited_providers.to_a.join(", ")
|
|
198
229
|
|
|
199
|
-
msg = "All configured providers unavailable. "
|
|
200
|
-
msg
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
msg
|
|
205
|
-
msg
|
|
230
|
+
msg = +"All configured providers unavailable. "
|
|
231
|
+
msg << "Tried: #{providers_tried}. "
|
|
232
|
+
if (evidence = @last_execution_evidence)
|
|
233
|
+
msg << "\nLast execution evidence: #{evidence.summary}. "
|
|
234
|
+
end
|
|
235
|
+
msg << "\nTry:\n"
|
|
236
|
+
msg << " - Check provider status pages\n"
|
|
237
|
+
msg << " - Configure additional providers\n"
|
|
238
|
+
msg << " - Retry in a few minutes\n"
|
|
239
|
+
msg << " - Run with --debug for detailed errors"
|
|
206
240
|
msg
|
|
207
241
|
end
|
|
208
242
|
|
data/lib/ace/llm/version.rb
CHANGED
data/lib/ace/llm.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative "llm/atoms/http_client"
|
|
|
18
18
|
require_relative "llm/atoms/xdg_directory_resolver"
|
|
19
19
|
require_relative "llm/atoms/error_classifier"
|
|
20
20
|
|
|
21
|
+
require_relative "llm/models/execution_evidence"
|
|
21
22
|
require_relative "llm/models/fallback_config"
|
|
22
23
|
require_relative "llm/models/role_config"
|
|
23
24
|
|
|
@@ -51,7 +52,15 @@ require_relative "llm/cli"
|
|
|
51
52
|
module Ace
|
|
52
53
|
module LLM
|
|
53
54
|
class Error < StandardError; end
|
|
54
|
-
|
|
55
|
+
|
|
56
|
+
# Provider failure carrying optional structured execution evidence
|
|
57
|
+
# (Ace::LLM::Models::ExecutionEvidence) about how the provider session
|
|
58
|
+
# actually ended. Evidence is attached at the subprocess boundary; absence
|
|
59
|
+
# keeps legacy message-based classification behavior.
|
|
60
|
+
class ProviderError < Error
|
|
61
|
+
attr_accessor :execution_evidence
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
class ConfigurationError < Error; end
|
|
56
65
|
class AuthenticationError < Error; end
|
|
57
66
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-llm
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-09-
|
|
10
|
+
date: 2026-09-30 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: ace-support-cli
|
|
@@ -288,6 +288,7 @@ files:
|
|
|
288
288
|
- lib/ace/llm/cli.rb
|
|
289
289
|
- lib/ace/llm/cli/commands/query.rb
|
|
290
290
|
- lib/ace/llm/configuration.rb
|
|
291
|
+
- lib/ace/llm/models/execution_evidence.rb
|
|
291
292
|
- lib/ace/llm/models/fallback_config.rb
|
|
292
293
|
- lib/ace/llm/models/role_config.rb
|
|
293
294
|
- lib/ace/llm/molecules/client_registry.rb
|