agent-cli-runtime 0.2.0 → 0.2.4
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 +82 -15
- data/README.md +54 -18
- data/agent-cli-runtime.gemspec +7 -6
- data/lib/agent_cli_runtime/error_extractors.rb +95 -0
- data/lib/agent_cli_runtime/opencode/overlay.rb +35 -120
- data/lib/agent_cli_runtime/opencode/permissions.rb +144 -0
- data/lib/agent_cli_runtime/opencode/probe.rb +40 -16
- data/lib/agent_cli_runtime/opencode/result_parser.rb +24 -10
- data/lib/agent_cli_runtime/profile.rb +28 -13
- data/lib/agent_cli_runtime/profiles.rb +28 -2
- data/lib/agent_cli_runtime/runtime.rb +102 -11
- data/lib/agent_cli_runtime/usage_extractors.rb +119 -32
- data/lib/agent_cli_runtime/values.rb +54 -8
- data/lib/agent_cli_runtime/version.rb +1 -1
- data/lib/agent_cli_runtime.rb +6 -0
- metadata +9 -7
|
@@ -46,12 +46,23 @@ module AgentCliRuntime
|
|
|
46
46
|
end
|
|
47
47
|
argv.concat(request.trusted_cli_arguments)
|
|
48
48
|
argv.concat(profile.output_format_flags) if request.include_output_format
|
|
49
|
-
|
|
50
|
-
prompt_style
|
|
49
|
+
stdin_data =
|
|
50
|
+
case prompt_style
|
|
51
|
+
when :positional
|
|
52
|
+
argv << request.prompt
|
|
53
|
+
nil
|
|
54
|
+
when :headless_flag_value
|
|
55
|
+
nil
|
|
56
|
+
when :stdin
|
|
57
|
+
argv << "-"
|
|
58
|
+
request.prompt
|
|
59
|
+
when :piped_stdin
|
|
60
|
+
request.prompt
|
|
61
|
+
end
|
|
51
62
|
|
|
52
63
|
CompiledInvocation.new(
|
|
53
64
|
argv: request.command_prefix + argv,
|
|
54
|
-
stdin_data
|
|
65
|
+
stdin_data:,
|
|
55
66
|
provider: profile.name,
|
|
56
67
|
launcher_identity: profile.launcher_identity,
|
|
57
68
|
capability_evidence: evidence
|
|
@@ -107,6 +118,51 @@ module AgentCliRuntime
|
|
|
107
118
|
end
|
|
108
119
|
end
|
|
109
120
|
|
|
121
|
+
# A provider failure seen mid-stream, normalized to
|
|
122
|
+
# {kind:, provider:, status_code:, message:}, or nil when the event is
|
|
123
|
+
# clean. Callers use this to tell a provider-side stop (quota, credit
|
|
124
|
+
# ceiling, rate limit, model output truncation) apart from an agent that
|
|
125
|
+
# genuinely produced nothing: several CLIs report the former on the stream
|
|
126
|
+
# and still exit zero.
|
|
127
|
+
def extract_provider_error(profile, event)
|
|
128
|
+
resolved = Profiles.resolve(profile)
|
|
129
|
+
extracted = resolved.extract_error_event(event)
|
|
130
|
+
return nil if extracted.nil?
|
|
131
|
+
|
|
132
|
+
kind, text = normalize_extracted_error(extracted)
|
|
133
|
+
return nil if text.nil? || text.strip.empty?
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
kind: kind,
|
|
137
|
+
provider: resolved.name,
|
|
138
|
+
status_code: status_code_from(text),
|
|
139
|
+
message: Redactor.diagnostic(text)
|
|
140
|
+
}.freeze
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def normalize_extracted_error(extracted)
|
|
144
|
+
return [ extracted.kind, extracted.message ] if extracted.is_a?(ExtractedFailure)
|
|
145
|
+
return [ failure_kind_from(extracted), extracted ] if extracted.is_a?(String)
|
|
146
|
+
|
|
147
|
+
[ nil, nil ]
|
|
148
|
+
end
|
|
149
|
+
private_class_method :normalize_extracted_error
|
|
150
|
+
|
|
151
|
+
def failure_kind_from(text)
|
|
152
|
+
status = status_code_from(text)
|
|
153
|
+
return :provider_limit if status == 402
|
|
154
|
+
return :rate_limited if status == 429
|
|
155
|
+
|
|
156
|
+
normalized = text.to_s.downcase
|
|
157
|
+
return :rate_limited if normalized.match?(/\brate[\s_-]*limit(?:ed|s)?\b/)
|
|
158
|
+
return :provider_limit if normalized.match?(
|
|
159
|
+
/\b(?:quota|billing|credits?|tokens?[\s_-]*limit|usage[\s_-]*limit)\b/
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
:provider_error
|
|
163
|
+
end
|
|
164
|
+
private_class_method :failure_kind_from
|
|
165
|
+
|
|
110
166
|
def observe(profile, result)
|
|
111
167
|
resolved = Profiles.resolve(profile)
|
|
112
168
|
raw = result.is_a?(Hash) ? result : {}
|
|
@@ -267,22 +323,57 @@ module AgentCliRuntime
|
|
|
267
323
|
end
|
|
268
324
|
private_class_method :tool_csv
|
|
269
325
|
|
|
326
|
+
# Providers prefix the status onto the text ("402: {...}") or carry it in
|
|
327
|
+
# the embedded payload. Either is enough to classify without matching on
|
|
328
|
+
# human-readable wording, which differs per provider and changes freely.
|
|
329
|
+
def status_code_from(text)
|
|
330
|
+
value = text[/\A\s*(\d{3})\s*:/, 1] ||
|
|
331
|
+
text[/"(?:code|http_status)"\s*:\s*(\d{3})\b/, 1]
|
|
332
|
+
code = value.to_i
|
|
333
|
+
code.between?(100, 599) ? code : nil
|
|
334
|
+
end
|
|
335
|
+
private_class_method :status_code_from
|
|
336
|
+
|
|
270
337
|
def normalize_usage(usage)
|
|
271
338
|
return nil unless usage.is_a?(Hash)
|
|
272
339
|
|
|
273
|
-
|
|
274
|
-
output = usage.key?(:output) ? usage[:output] : usage["output"]
|
|
275
|
-
cached = usage.key?(:cached) ? usage[:cached] : usage["cached"]
|
|
276
|
-
model = usage.key?(:model) ? usage[:model] : usage["model"]
|
|
340
|
+
value = ->(key) { usage.key?(key) ? usage[key] : usage[key.to_s] }
|
|
277
341
|
{
|
|
278
|
-
input:
|
|
279
|
-
output:
|
|
280
|
-
cached:
|
|
281
|
-
|
|
342
|
+
input: normalized_count(value.call(:input)),
|
|
343
|
+
output: normalized_count(value.call(:output)),
|
|
344
|
+
cached: normalized_count(value.call(:cached)),
|
|
345
|
+
cache_read: normalized_count(value.call(:cache_read)),
|
|
346
|
+
cache_write: normalized_count(value.call(:cache_write)),
|
|
347
|
+
reasoning: normalized_count(value.call(:reasoning)),
|
|
348
|
+
input_includes_cache_read: normalized_boolean(value.call(:input_includes_cache_read)),
|
|
349
|
+
input_includes_cache_write: normalized_boolean(value.call(:input_includes_cache_write)),
|
|
350
|
+
output_includes_reasoning: normalized_boolean(value.call(:output_includes_reasoning)),
|
|
351
|
+
model: value.call(:model)&.to_s&.dup&.freeze,
|
|
352
|
+
# Carried as a float: this is money, and normalized_count would round
|
|
353
|
+
# a fraction-of-a-cent charge to zero.
|
|
354
|
+
provider_reported_cost: normalized_cost(value.call(:provider_reported_cost))
|
|
282
355
|
}.freeze
|
|
283
356
|
end
|
|
284
357
|
private_class_method :normalize_usage
|
|
285
358
|
|
|
359
|
+
def normalized_cost(value)
|
|
360
|
+
return nil unless value.is_a?(Numeric)
|
|
361
|
+
|
|
362
|
+
cost = value.to_f
|
|
363
|
+
cost if cost >= 0 && cost.finite?
|
|
364
|
+
end
|
|
365
|
+
private_class_method :normalized_cost
|
|
366
|
+
|
|
367
|
+
def normalized_count(value)
|
|
368
|
+
value.nil? ? nil : [ value.to_i, 0 ].max
|
|
369
|
+
end
|
|
370
|
+
private_class_method :normalized_count
|
|
371
|
+
|
|
372
|
+
def normalized_boolean(value)
|
|
373
|
+
value if value == true || value == false
|
|
374
|
+
end
|
|
375
|
+
private_class_method :normalized_boolean
|
|
376
|
+
|
|
286
377
|
def unsupported!(profile, capability, diagnostic)
|
|
287
378
|
evidence = unsupported_evidence(profile, capability, diagnostic)
|
|
288
379
|
raise UnsupportedCapability.new(
|
|
@@ -7,9 +7,9 @@ module AgentCliRuntime
|
|
|
7
7
|
|
|
8
8
|
usage = usage_hash(event)
|
|
9
9
|
if event["type"] == "result" && usage
|
|
10
|
-
usage_result(event, usage, model_from(event))
|
|
10
|
+
usage_result(event, usage, model_from(event), provider: :claude)
|
|
11
11
|
elsif event["type"] == "stream_event" && usage
|
|
12
|
-
usage_result(event, usage, model_from(event))
|
|
12
|
+
usage_result(event, usage, model_from(event), provider: :claude)
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -17,21 +17,21 @@ module AgentCliRuntime
|
|
|
17
17
|
next nil unless event.is_a?(Hash)
|
|
18
18
|
|
|
19
19
|
usage = usage_hash(event)
|
|
20
|
-
usage_result(event, usage, model_from(event)) if usage
|
|
20
|
+
usage_result(event, usage, model_from(event), provider: :codex) if usage
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
PI = lambda do |event|
|
|
24
24
|
next nil unless event.is_a?(Hash)
|
|
25
25
|
|
|
26
26
|
usage = usage_hash(event)
|
|
27
|
-
usage_result(event, usage, model_from(event)) if usage
|
|
27
|
+
usage_result(event, usage, model_from(event), provider: :pi) if usage
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
GROK = lambda do |event|
|
|
31
31
|
next nil unless event.is_a?(Hash)
|
|
32
32
|
|
|
33
33
|
usage = usage_hash(event)
|
|
34
|
-
usage_result(event, usage, model_from(event)) if usage
|
|
34
|
+
usage_result(event, usage, model_from(event), provider: :grok) if usage
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def usage_hash(event)
|
|
@@ -40,6 +40,9 @@ module AgentCliRuntime
|
|
|
40
40
|
event["token_usage"],
|
|
41
41
|
event.dig("event", "usage"),
|
|
42
42
|
event.dig("event", "message", "usage"),
|
|
43
|
+
# pi reports usage on the assistant message itself, not under an
|
|
44
|
+
# "event" envelope. Without this every pi run is unmetered.
|
|
45
|
+
event.dig("message", "usage"),
|
|
43
46
|
event.dig("info", "usage"),
|
|
44
47
|
event.dig("info", "total_token_usage"),
|
|
45
48
|
event.dig("response", "usage"),
|
|
@@ -48,42 +51,126 @@ module AgentCliRuntime
|
|
|
48
51
|
candidates.find { |value| value.is_a?(Hash) }
|
|
49
52
|
end
|
|
50
53
|
|
|
51
|
-
def usage_result(event, usage, model)
|
|
54
|
+
def usage_result(event, usage, model, provider:)
|
|
55
|
+
cache_read = cache_read_tokens(usage)
|
|
56
|
+
cache_write = cache_write_tokens(usage)
|
|
57
|
+
aggregate_cached = aggregate_cached_tokens(usage)
|
|
58
|
+
reasoning = optional_token_count(
|
|
59
|
+
usage, "reasoning_tokens", "reasoningTokens"
|
|
60
|
+
)
|
|
61
|
+
reasoning ||= optional_token_count(
|
|
62
|
+
nested_hash(usage, "completion_tokens_details"),
|
|
63
|
+
"reasoning_tokens", "reasoningTokens"
|
|
64
|
+
)
|
|
52
65
|
{
|
|
53
|
-
input
|
|
54
|
-
|
|
66
|
+
# The bare "input"/"output" spellings are pi's and are matched last,
|
|
67
|
+
# so a provider using an explicit *_tokens key keeps its own reading.
|
|
68
|
+
input: optional_token_count(
|
|
69
|
+
usage, "input_tokens", "inputTokens", "prompt_tokens", "promptTokens",
|
|
70
|
+
"input"
|
|
55
71
|
),
|
|
56
|
-
output:
|
|
72
|
+
output: optional_token_count(
|
|
57
73
|
usage, "output_tokens", "outputTokens", "completion_tokens",
|
|
58
|
-
"completionTokens"
|
|
74
|
+
"completionTokens", "output"
|
|
59
75
|
),
|
|
60
|
-
cached:
|
|
61
|
-
|
|
76
|
+
cached: aggregate_cached || complete_cached(cache_read, cache_write),
|
|
77
|
+
cache_read: cache_read,
|
|
78
|
+
cache_write: cache_write,
|
|
79
|
+
reasoning: reasoning,
|
|
80
|
+
input_includes_cache_read:
|
|
81
|
+
inclusion_or_inferred(
|
|
82
|
+
usage, "input_includes_cache_read",
|
|
83
|
+
inferred_input_cache_inclusion(provider, cache_read)
|
|
84
|
+
),
|
|
85
|
+
input_includes_cache_write:
|
|
86
|
+
inclusion_or_inferred(
|
|
87
|
+
usage, "input_includes_cache_write",
|
|
88
|
+
inferred_input_cache_inclusion(provider, cache_write)
|
|
89
|
+
),
|
|
90
|
+
output_includes_reasoning:
|
|
91
|
+
inclusion_or_inferred(
|
|
92
|
+
usage, "output_includes_reasoning",
|
|
93
|
+
inferred_reasoning_inclusion(provider, reasoning)
|
|
94
|
+
),
|
|
95
|
+
model: model || model_from_usage(usage) || model_from(event),
|
|
96
|
+
provider_reported_cost: reported_cost(usage)
|
|
62
97
|
}
|
|
63
98
|
end
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
100
|
+
# A charge the provider itself reported for this turn, in USD. This is an
|
|
101
|
+
# observed amount, not an estimate from the pricing catalog, and it is the
|
|
102
|
+
# only cost available for a model the catalog does not carry — an
|
|
103
|
+
# OpenRouter-routed model, for one. OpenRouter sends a per-category
|
|
104
|
+
# breakdown carrying a total; other providers may send a scalar.
|
|
105
|
+
def reported_cost(usage)
|
|
106
|
+
value = usage["cost"] || usage["total_cost"] || usage["totalCost"]
|
|
107
|
+
value = value["total"] || value["total_cost"] if value.is_a?(Hash)
|
|
108
|
+
return nil unless value.is_a?(Numeric) || value.is_a?(String)
|
|
109
|
+
|
|
110
|
+
cost = begin
|
|
111
|
+
Float(value)
|
|
112
|
+
rescue ArgumentError, TypeError
|
|
113
|
+
nil
|
|
114
|
+
end
|
|
115
|
+
cost if cost && cost >= 0 && cost.finite?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def aggregate_cached_tokens(usage)
|
|
119
|
+
optional_token_count(
|
|
67
120
|
usage, "cached", "cached_tokens", "cachedTokens",
|
|
68
121
|
"cached_input_tokens", "cachedInputTokens"
|
|
69
122
|
)
|
|
70
|
-
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def cache_read_tokens(usage)
|
|
126
|
+
optional_token_count(
|
|
127
|
+
usage, "cache_read_input_tokens", "cacheReadInputTokens"
|
|
128
|
+
) || optional_token_count(
|
|
129
|
+
nested_hash(usage, "prompt_tokens_details"), "cached_tokens", "cachedTokens"
|
|
130
|
+
) || optional_token_count(
|
|
131
|
+
nested_hash(usage, "input_tokens_details"), "cached_tokens", "cachedTokens"
|
|
132
|
+
) || optional_token_count(usage, "cacheRead")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def cache_write_tokens(usage)
|
|
136
|
+
optional_token_count(
|
|
137
|
+
usage, "cache_creation_input_tokens", "cacheCreationInputTokens",
|
|
138
|
+
"cache_write_input_tokens", "cacheWriteInputTokens", "cacheWrite"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
71
141
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
) +
|
|
77
|
-
token_count(
|
|
78
|
-
nested_hash(usage, "input_tokens_details"), "cached_tokens", "cachedTokens"
|
|
79
|
-
)
|
|
142
|
+
def complete_cached(cache_read, cache_write)
|
|
143
|
+
return nil if cache_read.nil? || cache_write.nil?
|
|
144
|
+
|
|
145
|
+
cache_read + cache_write
|
|
80
146
|
end
|
|
81
147
|
|
|
82
|
-
def
|
|
83
|
-
return
|
|
148
|
+
def inferred_input_cache_inclusion(provider, value)
|
|
149
|
+
return nil if value.nil?
|
|
150
|
+
return false if provider == :claude
|
|
151
|
+
return true if provider == :codex
|
|
152
|
+
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def inferred_reasoning_inclusion(provider, value)
|
|
157
|
+
return nil if value.nil?
|
|
158
|
+
|
|
159
|
+
provider == :codex ? true : nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def inclusion_or_inferred(hash, key, inferred)
|
|
163
|
+
return inferred unless hash.is_a?(Hash) && hash.key?(key)
|
|
164
|
+
|
|
165
|
+
value = hash[key]
|
|
166
|
+
value if value == true || value == false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def optional_token_count(hash, *keys)
|
|
170
|
+
return nil unless hash.is_a?(Hash)
|
|
84
171
|
|
|
85
172
|
key = keys.find { |candidate| hash.key?(candidate) }
|
|
86
|
-
key ? hash[key].to_i :
|
|
173
|
+
key ? hash[key].to_i : nil
|
|
87
174
|
end
|
|
88
175
|
|
|
89
176
|
def nested_hash(hash, key)
|
|
@@ -92,11 +179,6 @@ module AgentCliRuntime
|
|
|
92
179
|
end
|
|
93
180
|
|
|
94
181
|
def model_from(event)
|
|
95
|
-
model_usage = event["modelUsage"]
|
|
96
|
-
if model_usage.is_a?(Hash) && !model_usage.empty?
|
|
97
|
-
return model_usage.keys.first.to_s
|
|
98
|
-
end
|
|
99
|
-
|
|
100
182
|
candidates = [
|
|
101
183
|
event["model"],
|
|
102
184
|
event.dig("message", "model"),
|
|
@@ -104,7 +186,12 @@ module AgentCliRuntime
|
|
|
104
186
|
event.dig("response", "model"),
|
|
105
187
|
event.dig("item", "model")
|
|
106
188
|
]
|
|
107
|
-
candidates.find { |value| !value.to_s.empty? }
|
|
189
|
+
explicit = candidates.find { |value| !value.to_s.empty? }
|
|
190
|
+
return explicit.to_s if explicit
|
|
191
|
+
|
|
192
|
+
# Run totals may include helper models; their order is not agent identity.
|
|
193
|
+
model_usage = event["modelUsage"]
|
|
194
|
+
model_usage.keys.first.to_s if model_usage.is_a?(Hash) && model_usage.size == 1
|
|
108
195
|
end
|
|
109
196
|
|
|
110
197
|
def model_from_usage(usage)
|
|
@@ -81,6 +81,12 @@ module AgentCliRuntime
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
ExtractedFailure = Data.define(:kind, :message) do
|
|
85
|
+
def initialize(kind:, message:)
|
|
86
|
+
super(kind: kind.to_sym, message: Immutable.string(message))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
84
90
|
Request = Data.define(
|
|
85
91
|
:profile, :prompt, :permission_mode, :permission_arguments,
|
|
86
92
|
:add_dirs, :require_add_dirs,
|
|
@@ -265,13 +271,14 @@ module AgentCliRuntime
|
|
|
265
271
|
:configuration_path, :configuration,
|
|
266
272
|
:credential_environment_keys, :credential_file,
|
|
267
273
|
:permission_policy, :additional_read_roots,
|
|
268
|
-
:additional_write_roots, :edit_patterns, :plugins, :pure
|
|
274
|
+
:additional_write_roots, :edit_patterns, :bash_patterns, :plugins, :pure
|
|
269
275
|
) do
|
|
270
276
|
def initialize(request:, working_directory:, invocation_root:,
|
|
271
277
|
configuration_path: nil, configuration: nil,
|
|
272
278
|
credential_environment_keys: [], credential_file: nil,
|
|
273
279
|
permission_policy: nil, additional_read_roots: [],
|
|
274
|
-
additional_write_roots: [], edit_patterns: [],
|
|
280
|
+
additional_write_roots: [], edit_patterns: [],
|
|
281
|
+
bash_patterns: [], plugins: [], pure: true)
|
|
275
282
|
unless request.is_a?(Request)
|
|
276
283
|
raise ArgumentError, "request must be an AgentCliRuntime::Request"
|
|
277
284
|
end
|
|
@@ -294,6 +301,10 @@ module AgentCliRuntime
|
|
|
294
301
|
raise ArgumentError,
|
|
295
302
|
"credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
|
|
296
303
|
end
|
|
304
|
+
normalized_bash_patterns = Immutable.strings(bash_patterns)
|
|
305
|
+
if normalized_bash_patterns.any?(&:empty?)
|
|
306
|
+
raise ArgumentError, "OpenCode bash patterns must be non-empty"
|
|
307
|
+
end
|
|
297
308
|
|
|
298
309
|
super(
|
|
299
310
|
request: request,
|
|
@@ -310,6 +321,7 @@ module AgentCliRuntime
|
|
|
310
321
|
additional_read_roots: Immutable.strings(additional_read_roots),
|
|
311
322
|
additional_write_roots: Immutable.strings(additional_write_roots),
|
|
312
323
|
edit_patterns: Immutable.strings(edit_patterns),
|
|
324
|
+
bash_patterns: normalized_bash_patterns,
|
|
313
325
|
plugins: Immutable.strings(plugins),
|
|
314
326
|
pure: pure != false
|
|
315
327
|
)
|
|
@@ -318,11 +330,14 @@ module AgentCliRuntime
|
|
|
318
330
|
|
|
319
331
|
ProbeRequest = Data.define(
|
|
320
332
|
:profile, :route, :variant, :environment,
|
|
321
|
-
:credential_environment_keys, :credential_file_staged
|
|
333
|
+
:credential_environment_keys, :credential_file_staged,
|
|
334
|
+
:configured_variants, :executable
|
|
322
335
|
) do
|
|
323
336
|
def initialize(profile:, route:, variant: nil, environment: {},
|
|
324
337
|
credential_environment_keys: [],
|
|
325
|
-
credential_file_staged: false
|
|
338
|
+
credential_file_staged: false,
|
|
339
|
+
configured_variants: nil,
|
|
340
|
+
executable: nil)
|
|
326
341
|
parsed_route = route.is_a?(Route) ? route : Route.parse(route)
|
|
327
342
|
super(
|
|
328
343
|
profile: profile,
|
|
@@ -331,7 +346,10 @@ module AgentCliRuntime
|
|
|
331
346
|
environment: Immutable.hash(environment),
|
|
332
347
|
credential_environment_keys:
|
|
333
348
|
Immutable.strings(credential_environment_keys),
|
|
334
|
-
credential_file_staged: credential_file_staged == true
|
|
349
|
+
credential_file_staged: credential_file_staged == true,
|
|
350
|
+
configured_variants:
|
|
351
|
+
configured_variants.nil? ? nil : Immutable.strings(configured_variants),
|
|
352
|
+
executable: executable.nil? ? nil : Immutable.string(executable)
|
|
335
353
|
)
|
|
336
354
|
end
|
|
337
355
|
end
|
|
@@ -445,17 +463,35 @@ module AgentCliRuntime
|
|
|
445
463
|
end
|
|
446
464
|
|
|
447
465
|
NormalizedUsage = Data.define(
|
|
448
|
-
:input, :output, :cache_read, :cache_write, :reasoning,
|
|
466
|
+
:input, :output, :cache_read, :cache_write, :reasoning,
|
|
467
|
+
:input_includes_cache_read, :input_includes_cache_write,
|
|
468
|
+
:output_includes_reasoning, :cost
|
|
449
469
|
) do
|
|
450
470
|
def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil,
|
|
451
|
-
reasoning: nil,
|
|
471
|
+
reasoning: nil, input_includes_cache_read: nil,
|
|
472
|
+
input_includes_cache_write: nil,
|
|
473
|
+
output_includes_reasoning: nil,
|
|
474
|
+
provider_reported_cost: nil, cost: nil)
|
|
475
|
+
if !provider_reported_cost.nil? && !cost.nil? && provider_reported_cost != cost
|
|
476
|
+
raise ArgumentError, "provider_reported_cost and cost disagree"
|
|
477
|
+
end
|
|
452
478
|
super(
|
|
453
479
|
input: number(input, :input, integer: true),
|
|
454
480
|
output: number(output, :output, integer: true),
|
|
455
481
|
cache_read: number(cache_read, :cache_read, integer: true),
|
|
456
482
|
cache_write: number(cache_write, :cache_write, integer: true),
|
|
457
483
|
reasoning: number(reasoning, :reasoning, integer: true),
|
|
458
|
-
|
|
484
|
+
input_includes_cache_read:
|
|
485
|
+
boolean(input_includes_cache_read, :input_includes_cache_read),
|
|
486
|
+
input_includes_cache_write:
|
|
487
|
+
boolean(input_includes_cache_write, :input_includes_cache_write),
|
|
488
|
+
output_includes_reasoning:
|
|
489
|
+
boolean(output_includes_reasoning, :output_includes_reasoning),
|
|
490
|
+
cost: number(
|
|
491
|
+
provider_reported_cost.nil? ? cost : provider_reported_cost,
|
|
492
|
+
:provider_reported_cost,
|
|
493
|
+
integer: false
|
|
494
|
+
)
|
|
459
495
|
)
|
|
460
496
|
end
|
|
461
497
|
|
|
@@ -465,6 +501,8 @@ module AgentCliRuntime
|
|
|
465
501
|
cache_read + cache_write
|
|
466
502
|
end
|
|
467
503
|
|
|
504
|
+
alias provider_reported_cost cost
|
|
505
|
+
|
|
468
506
|
private
|
|
469
507
|
|
|
470
508
|
def number(value, label, integer:)
|
|
@@ -477,6 +515,14 @@ module AgentCliRuntime
|
|
|
477
515
|
|
|
478
516
|
value
|
|
479
517
|
end
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def boolean(value, label)
|
|
521
|
+
return nil if value.nil?
|
|
522
|
+
return value if value == true || value == false
|
|
523
|
+
|
|
524
|
+
raise ArgumentError, "#{label} must be true, false, or nil"
|
|
525
|
+
end
|
|
480
526
|
end
|
|
481
527
|
|
|
482
528
|
RouteIdentity = Data.define(:requested, :actual, :resolution_status) do
|
data/lib/agent_cli_runtime.rb
CHANGED
|
@@ -8,12 +8,14 @@ end
|
|
|
8
8
|
|
|
9
9
|
require "agent_cli_runtime/redactor"
|
|
10
10
|
require "agent_cli_runtime/usage_extractors"
|
|
11
|
+
require "agent_cli_runtime/error_extractors"
|
|
11
12
|
require "agent_cli_runtime/opencode/result_parser"
|
|
12
13
|
require "agent_cli_runtime/profile"
|
|
13
14
|
require "agent_cli_runtime/profiles"
|
|
14
15
|
require "agent_cli_runtime/probe"
|
|
15
16
|
require "agent_cli_runtime/runtime"
|
|
16
17
|
require "agent_cli_runtime/opencode/probe"
|
|
18
|
+
require "agent_cli_runtime/opencode/permissions"
|
|
17
19
|
require "agent_cli_runtime/opencode/overlay"
|
|
18
20
|
require "agent_cli_runtime/opencode/inspection"
|
|
19
21
|
require "agent_cli_runtime/cli"
|
|
@@ -37,6 +39,10 @@ module AgentCliRuntime
|
|
|
37
39
|
Runtime.extract_usage(profile, event)
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
def extract_provider_error(profile, event)
|
|
43
|
+
Runtime.extract_provider_error(profile, event)
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
def observe(profile, result)
|
|
41
47
|
Runtime.observe(profile, result)
|
|
42
48
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-cli-runtime
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Kuznetsov
|
|
@@ -64,11 +64,11 @@ dependencies:
|
|
|
64
64
|
- !ruby/object:Gem::Version
|
|
65
65
|
version: '1.0'
|
|
66
66
|
description: |
|
|
67
|
-
Agent CLI Runtime
|
|
68
|
-
local
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
Agent CLI Runtime gives Ruby applications one stable integration layer for
|
|
68
|
+
local coding-agent CLIs. It builds provider-specific commands from a shared
|
|
69
|
+
request model, checks local versions and named capabilities, and normalizes
|
|
70
|
+
usage and results afterward. Centralized profiles, environment rules, and
|
|
71
|
+
parsers make agent providers easier to add, switch, and upgrade.
|
|
72
72
|
email:
|
|
73
73
|
- ivan@ikuznetsov.com
|
|
74
74
|
executables:
|
|
@@ -83,9 +83,11 @@ files:
|
|
|
83
83
|
- exe/agent-runtime
|
|
84
84
|
- lib/agent_cli_runtime.rb
|
|
85
85
|
- lib/agent_cli_runtime/cli.rb
|
|
86
|
+
- lib/agent_cli_runtime/error_extractors.rb
|
|
86
87
|
- lib/agent_cli_runtime/errors.rb
|
|
87
88
|
- lib/agent_cli_runtime/opencode/inspection.rb
|
|
88
89
|
- lib/agent_cli_runtime/opencode/overlay.rb
|
|
90
|
+
- lib/agent_cli_runtime/opencode/permissions.rb
|
|
89
91
|
- lib/agent_cli_runtime/opencode/probe.rb
|
|
90
92
|
- lib/agent_cli_runtime/opencode/result_parser.rb
|
|
91
93
|
- lib/agent_cli_runtime/probe.rb
|
|
@@ -122,5 +124,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
122
124
|
requirements: []
|
|
123
125
|
rubygems_version: 3.6.9
|
|
124
126
|
specification_version: 4
|
|
125
|
-
summary:
|
|
127
|
+
summary: One Ruby API for Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode
|
|
126
128
|
test_files: []
|