agent-cli-runtime 0.1.1 → 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 +87 -0
- data/README.md +177 -9
- data/agent-cli-runtime.gemspec +7 -5
- data/lib/agent_cli_runtime/error_extractors.rb +95 -0
- data/lib/agent_cli_runtime/errors.rb +7 -0
- data/lib/agent_cli_runtime/opencode/inspection.rb +32 -0
- data/lib/agent_cli_runtime/opencode/overlay.rb +488 -0
- data/lib/agent_cli_runtime/opencode/permissions.rb +144 -0
- data/lib/agent_cli_runtime/opencode/probe.rb +304 -0
- data/lib/agent_cli_runtime/opencode/result_parser.rb +416 -0
- data/lib/agent_cli_runtime/profile.rb +60 -11
- data/lib/agent_cli_runtime/profiles.rb +98 -3
- data/lib/agent_cli_runtime/runtime.rb +118 -13
- data/lib/agent_cli_runtime/usage_extractors.rb +119 -32
- data/lib/agent_cli_runtime/values.rb +478 -0
- data/lib/agent_cli_runtime/version.rb +1 -1
- data/lib/agent_cli_runtime.rb +29 -3
- metadata +13 -6
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
module AgentCliRuntime
|
|
2
|
+
OPENCODE_OVERLAY_ENVIRONMENT_KEYS = %w[
|
|
3
|
+
XDG_CONFIG_HOME XDG_DATA_HOME XDG_CACHE_HOME XDG_STATE_HOME TMPDIR
|
|
4
|
+
OPENCODE_CONFIG OPENCODE_DISABLE_PROJECT_CONFIG
|
|
5
|
+
OPENCODE_DISABLE_CLAUDE_CODE OPENCODE_DISABLE_MODELS_FETCH
|
|
6
|
+
OPENCODE_DISABLE_AUTOUPDATE OPENCODE_PURE
|
|
7
|
+
].freeze
|
|
8
|
+
|
|
2
9
|
module Immutable
|
|
3
10
|
module_function
|
|
4
11
|
|
|
@@ -14,6 +21,28 @@ module AgentCliRuntime
|
|
|
14
21
|
def symbols(values)
|
|
15
22
|
Array(values).map(&:to_sym).uniq.freeze
|
|
16
23
|
end
|
|
24
|
+
|
|
25
|
+
def hash(value)
|
|
26
|
+
unless value.is_a?(Hash)
|
|
27
|
+
raise ArgumentError, "value must be a Hash"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
value.each_with_object({}) do |(key, item), result|
|
|
31
|
+
result[string(key)] = deep(item)
|
|
32
|
+
end.freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def deep(value)
|
|
36
|
+
case value
|
|
37
|
+
when Hash then hash(value)
|
|
38
|
+
when Array then value.map { |item| deep(item) }.freeze
|
|
39
|
+
when String then string(value)
|
|
40
|
+
when Symbol, Numeric, TrueClass, FalseClass, NilClass then value
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError,
|
|
43
|
+
"unsupported immutable value #{value.class}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
17
46
|
end
|
|
18
47
|
private_constant :Immutable
|
|
19
48
|
|
|
@@ -52,6 +81,12 @@ module AgentCliRuntime
|
|
|
52
81
|
end
|
|
53
82
|
end
|
|
54
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
|
+
|
|
55
90
|
Request = Data.define(
|
|
56
91
|
:profile, :prompt, :permission_mode, :permission_arguments,
|
|
57
92
|
:add_dirs, :require_add_dirs,
|
|
@@ -173,4 +208,447 @@ module AgentCliRuntime
|
|
|
173
208
|
)
|
|
174
209
|
end
|
|
175
210
|
end
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
Route = Data.define(:provider, :model) do
|
|
214
|
+
ROUTE_PATTERN = /\A(?<provider>[a-zA-Z0-9][a-zA-Z0-9._-]*)\/(?<model>[^\s\/][^\s]*)\z/
|
|
215
|
+
|
|
216
|
+
def initialize(provider:, model:)
|
|
217
|
+
normalized_provider = Immutable.string(provider)
|
|
218
|
+
normalized_model = Immutable.string(model)
|
|
219
|
+
route = "#{normalized_provider}/#{normalized_model}"
|
|
220
|
+
unless ROUTE_PATTERN.match?(route) && !normalized_model.include?("\0")
|
|
221
|
+
raise ArgumentError,
|
|
222
|
+
"OpenCode route must be a full provider/model value"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
super(provider: normalized_provider, model: normalized_model)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self.parse(value)
|
|
229
|
+
match = ROUTE_PATTERN.match(value.to_s)
|
|
230
|
+
unless match
|
|
231
|
+
raise ArgumentError,
|
|
232
|
+
"OpenCode route must be a full provider/model value"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
new(provider: match[:provider], model: match[:model])
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def to_s
|
|
239
|
+
"#{provider}/#{model}"
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
OpenCodePermissionPolicy = Data.define(:rules) do
|
|
244
|
+
ACTIONS = %w[allow ask deny].freeze
|
|
245
|
+
|
|
246
|
+
def initialize(rules = nil, **keywords)
|
|
247
|
+
value = rules || keywords[:rules] || keywords
|
|
248
|
+
normalized = Immutable.hash(value || {})
|
|
249
|
+
raise ArgumentError, "OpenCode permission policy cannot be empty" if normalized.empty?
|
|
250
|
+
|
|
251
|
+
validate_actions!(normalized)
|
|
252
|
+
super(rules: normalized)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
private
|
|
256
|
+
|
|
257
|
+
def validate_actions!(value)
|
|
258
|
+
value.each_value do |entry|
|
|
259
|
+
if entry.is_a?(Hash)
|
|
260
|
+
validate_actions!(entry)
|
|
261
|
+
elsif !ACTIONS.include?(entry)
|
|
262
|
+
raise ArgumentError,
|
|
263
|
+
"OpenCode permission action must be allow, ask, or deny"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
OpenCodePreparationRequest = Data.define(
|
|
270
|
+
:request, :working_directory, :invocation_root,
|
|
271
|
+
:configuration_path, :configuration,
|
|
272
|
+
:credential_environment_keys, :credential_file,
|
|
273
|
+
:permission_policy, :additional_read_roots,
|
|
274
|
+
:additional_write_roots, :edit_patterns, :bash_patterns, :plugins, :pure
|
|
275
|
+
) do
|
|
276
|
+
def initialize(request:, working_directory:, invocation_root:,
|
|
277
|
+
configuration_path: nil, configuration: nil,
|
|
278
|
+
credential_environment_keys: [], credential_file: nil,
|
|
279
|
+
permission_policy: nil, additional_read_roots: [],
|
|
280
|
+
additional_write_roots: [], edit_patterns: [],
|
|
281
|
+
bash_patterns: [], plugins: [], pure: true)
|
|
282
|
+
unless request.is_a?(Request)
|
|
283
|
+
raise ArgumentError, "request must be an AgentCliRuntime::Request"
|
|
284
|
+
end
|
|
285
|
+
if configuration_path && configuration
|
|
286
|
+
raise ArgumentError,
|
|
287
|
+
"choose configuration_path or configuration, not both"
|
|
288
|
+
end
|
|
289
|
+
if permission_policy &&
|
|
290
|
+
!permission_policy.is_a?(OpenCodePermissionPolicy)
|
|
291
|
+
raise ArgumentError,
|
|
292
|
+
"permission_policy must be an OpenCodePermissionPolicy"
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
keys = Immutable.strings(credential_environment_keys)
|
|
296
|
+
invalid = keys.find { |key| !key.match?(/\A[A-Z][A-Z0-9_]*\z/) }
|
|
297
|
+
raise ArgumentError, "invalid credential environment key" if invalid
|
|
298
|
+
raise ArgumentError, "credential environment keys must be unique" if keys.uniq.length != keys.length
|
|
299
|
+
reserved = keys & OPENCODE_OVERLAY_ENVIRONMENT_KEYS
|
|
300
|
+
unless reserved.empty?
|
|
301
|
+
raise ArgumentError,
|
|
302
|
+
"credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
|
|
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
|
|
308
|
+
|
|
309
|
+
super(
|
|
310
|
+
request: request,
|
|
311
|
+
working_directory: Immutable.string(working_directory),
|
|
312
|
+
invocation_root: Immutable.string(invocation_root),
|
|
313
|
+
configuration_path:
|
|
314
|
+
configuration_path.nil? ? nil : Immutable.string(configuration_path),
|
|
315
|
+
configuration:
|
|
316
|
+
configuration.nil? ? nil : Immutable.hash(configuration),
|
|
317
|
+
credential_environment_keys: keys,
|
|
318
|
+
credential_file:
|
|
319
|
+
credential_file.nil? ? nil : Immutable.string(credential_file),
|
|
320
|
+
permission_policy: permission_policy,
|
|
321
|
+
additional_read_roots: Immutable.strings(additional_read_roots),
|
|
322
|
+
additional_write_roots: Immutable.strings(additional_write_roots),
|
|
323
|
+
edit_patterns: Immutable.strings(edit_patterns),
|
|
324
|
+
bash_patterns: normalized_bash_patterns,
|
|
325
|
+
plugins: Immutable.strings(plugins),
|
|
326
|
+
pure: pure != false
|
|
327
|
+
)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
ProbeRequest = Data.define(
|
|
332
|
+
:profile, :route, :variant, :environment,
|
|
333
|
+
:credential_environment_keys, :credential_file_staged,
|
|
334
|
+
:configured_variants, :executable
|
|
335
|
+
) do
|
|
336
|
+
def initialize(profile:, route:, variant: nil, environment: {},
|
|
337
|
+
credential_environment_keys: [],
|
|
338
|
+
credential_file_staged: false,
|
|
339
|
+
configured_variants: nil,
|
|
340
|
+
executable: nil)
|
|
341
|
+
parsed_route = route.is_a?(Route) ? route : Route.parse(route)
|
|
342
|
+
super(
|
|
343
|
+
profile: profile,
|
|
344
|
+
route: parsed_route,
|
|
345
|
+
variant: variant.nil? ? nil : Immutable.string(variant),
|
|
346
|
+
environment: Immutable.hash(environment),
|
|
347
|
+
credential_environment_keys:
|
|
348
|
+
Immutable.strings(credential_environment_keys),
|
|
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)
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
RouteProbeResult = Data.define(
|
|
358
|
+
:provider, :ready, :installed, :executable, :version,
|
|
359
|
+
:minimum_version, :auth_configuration, :route,
|
|
360
|
+
:route_available, :available_variants,
|
|
361
|
+
:capability_evidence, :diagnostic
|
|
362
|
+
) do
|
|
363
|
+
def initialize(provider:, ready:, installed:, executable:, version:,
|
|
364
|
+
minimum_version:, auth_configuration:, route:,
|
|
365
|
+
route_available:, available_variants:,
|
|
366
|
+
capability_evidence:, diagnostic: nil)
|
|
367
|
+
super(
|
|
368
|
+
provider: provider.to_sym,
|
|
369
|
+
ready: ready == true,
|
|
370
|
+
installed: installed == true,
|
|
371
|
+
executable: Immutable.string(executable),
|
|
372
|
+
version: version.nil? ? nil : Immutable.string(version),
|
|
373
|
+
minimum_version:
|
|
374
|
+
minimum_version.nil? ? nil : Immutable.string(minimum_version),
|
|
375
|
+
auth_configuration: auth_configuration,
|
|
376
|
+
route: route,
|
|
377
|
+
route_available: route_available == true,
|
|
378
|
+
available_variants: Immutable.strings(available_variants),
|
|
379
|
+
capability_evidence: Array(capability_evidence).freeze,
|
|
380
|
+
diagnostic:
|
|
381
|
+
diagnostic.nil? ? nil : Immutable.string(diagnostic)
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
class PreparedInvocation
|
|
387
|
+
attr_reader :invocation, :environment, :credential_environment_keys,
|
|
388
|
+
:invocation_root, :generated_paths, :configuration_path,
|
|
389
|
+
:requested_route, :configuration_source, :probe_result,
|
|
390
|
+
:executable
|
|
391
|
+
|
|
392
|
+
def initialize(invocation:, environment:, credential_environment_keys:,
|
|
393
|
+
invocation_root:, generated_paths:, configuration_path:,
|
|
394
|
+
requested_route:, configuration_source:, probe_result:,
|
|
395
|
+
cleanup:, executable: nil)
|
|
396
|
+
@invocation = invocation
|
|
397
|
+
@environment = Immutable.hash(environment)
|
|
398
|
+
@credential_environment_keys =
|
|
399
|
+
Immutable.strings(credential_environment_keys)
|
|
400
|
+
@invocation_root = Immutable.string(invocation_root)
|
|
401
|
+
@generated_paths = Immutable.strings(generated_paths)
|
|
402
|
+
@configuration_path = Immutable.string(configuration_path)
|
|
403
|
+
@requested_route = requested_route
|
|
404
|
+
@configuration_source =
|
|
405
|
+
configuration_source.nil? ? nil : Immutable.string(configuration_source)
|
|
406
|
+
@probe_result = probe_result
|
|
407
|
+
@executable = Immutable.string(executable || invocation.argv.fetch(0))
|
|
408
|
+
@cleanup = cleanup
|
|
409
|
+
freeze
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def environment_for(env: ENV)
|
|
413
|
+
selected = credential_environment_keys.each_with_object({}) do |key, values|
|
|
414
|
+
value = env[key]
|
|
415
|
+
values[key] = value.to_s unless value.to_s.empty?
|
|
416
|
+
end
|
|
417
|
+
selected.merge(environment).freeze
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def cleanup!
|
|
421
|
+
@cleanup.call
|
|
422
|
+
nil
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
TerminationEvidence = Data.define(
|
|
427
|
+
:exit_code, :timed_out, :cancelled, :signal
|
|
428
|
+
) do
|
|
429
|
+
def initialize(exit_code:, timed_out: false, cancelled: false, signal: nil)
|
|
430
|
+
unless exit_code.nil? || exit_code.is_a?(Integer)
|
|
431
|
+
raise ArgumentError, "exit_code must be an Integer or nil"
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
super(
|
|
435
|
+
exit_code: exit_code,
|
|
436
|
+
timed_out: timed_out == true,
|
|
437
|
+
cancelled: cancelled == true,
|
|
438
|
+
signal: signal.nil? ? nil : Immutable.string(signal)
|
|
439
|
+
)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def success?
|
|
443
|
+
!timed_out && !cancelled && signal.nil? && exit_code == 0
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
CapturedResult = Data.define(
|
|
448
|
+
:stdout, :stderr, :termination, :inspection_output
|
|
449
|
+
) do
|
|
450
|
+
def initialize(stdout:, stderr:, termination:, inspection_output: nil)
|
|
451
|
+
unless termination.is_a?(TerminationEvidence)
|
|
452
|
+
raise ArgumentError, "termination must be TerminationEvidence"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
super(
|
|
456
|
+
stdout: Immutable.string(stdout),
|
|
457
|
+
stderr: Immutable.string(stderr),
|
|
458
|
+
termination: termination,
|
|
459
|
+
inspection_output:
|
|
460
|
+
inspection_output.nil? ? nil : Immutable.string(inspection_output)
|
|
461
|
+
)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
NormalizedUsage = Data.define(
|
|
466
|
+
:input, :output, :cache_read, :cache_write, :reasoning,
|
|
467
|
+
:input_includes_cache_read, :input_includes_cache_write,
|
|
468
|
+
:output_includes_reasoning, :cost
|
|
469
|
+
) do
|
|
470
|
+
def initialize(input: nil, output: nil, cache_read: nil, cache_write: 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
|
|
478
|
+
super(
|
|
479
|
+
input: number(input, :input, integer: true),
|
|
480
|
+
output: number(output, :output, integer: true),
|
|
481
|
+
cache_read: number(cache_read, :cache_read, integer: true),
|
|
482
|
+
cache_write: number(cache_write, :cache_write, integer: true),
|
|
483
|
+
reasoning: number(reasoning, :reasoning, integer: true),
|
|
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
|
+
)
|
|
495
|
+
)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def cached
|
|
499
|
+
return nil if cache_read.nil? || cache_write.nil?
|
|
500
|
+
|
|
501
|
+
cache_read + cache_write
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
alias provider_reported_cost cost
|
|
505
|
+
|
|
506
|
+
private
|
|
507
|
+
|
|
508
|
+
def number(value, label, integer:)
|
|
509
|
+
return nil if value.nil?
|
|
510
|
+
valid = integer ? value.is_a?(Integer) : value.is_a?(Numeric)
|
|
511
|
+
valid &&= value.finite? if value.respond_to?(:finite?)
|
|
512
|
+
unless valid && value >= 0
|
|
513
|
+
raise ArgumentError, "#{label} must be a non-negative number or nil"
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
value
|
|
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
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
RouteIdentity = Data.define(:requested, :actual, :resolution_status) do
|
|
529
|
+
RESOLUTION_STATUSES = %i[unobserved matched resolved_differently].freeze
|
|
530
|
+
|
|
531
|
+
def initialize(requested:, actual: nil, resolution_status: nil)
|
|
532
|
+
requested_route = requested.is_a?(Route) ? requested : Route.parse(requested)
|
|
533
|
+
actual_route =
|
|
534
|
+
if actual.nil?
|
|
535
|
+
nil
|
|
536
|
+
elsif actual.is_a?(Route)
|
|
537
|
+
actual
|
|
538
|
+
else
|
|
539
|
+
Route.parse(actual)
|
|
540
|
+
end
|
|
541
|
+
status = resolution_status&.to_sym ||
|
|
542
|
+
(actual_route.nil? ? :unobserved :
|
|
543
|
+
(requested_route == actual_route ? :matched : :resolved_differently))
|
|
544
|
+
unless RESOLUTION_STATUSES.include?(status)
|
|
545
|
+
raise ArgumentError, "invalid route resolution status"
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
super(
|
|
549
|
+
requested: requested_route,
|
|
550
|
+
actual: actual_route,
|
|
551
|
+
resolution_status: status
|
|
552
|
+
)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
ParsedRun = Data.define(
|
|
557
|
+
:session_id, :terminal_message_id, :terminal_reason,
|
|
558
|
+
:final_message, :final_message_truncated, :preliminary_usage, :unknown_events
|
|
559
|
+
) do
|
|
560
|
+
def initialize(session_id:, terminal_message_id:, terminal_reason:,
|
|
561
|
+
final_message:, preliminary_usage:, unknown_events: [],
|
|
562
|
+
final_message_truncated: false)
|
|
563
|
+
unless preliminary_usage.is_a?(NormalizedUsage)
|
|
564
|
+
raise ArgumentError, "preliminary_usage must be NormalizedUsage"
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
super(
|
|
568
|
+
session_id: Immutable.string(session_id),
|
|
569
|
+
terminal_message_id: Immutable.string(terminal_message_id),
|
|
570
|
+
terminal_reason: Immutable.string(terminal_reason),
|
|
571
|
+
final_message: Immutable.string(final_message),
|
|
572
|
+
final_message_truncated: final_message_truncated == true,
|
|
573
|
+
preliminary_usage: preliminary_usage,
|
|
574
|
+
unknown_events: Immutable.strings(unknown_events)
|
|
575
|
+
)
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
InspectionCommand = Data.define(
|
|
580
|
+
:argv, :stdin_data, :environment, :credential_environment_keys,
|
|
581
|
+
:session_id, :message_id
|
|
582
|
+
) do
|
|
583
|
+
def initialize(argv:, environment:, credential_environment_keys:,
|
|
584
|
+
session_id:, message_id:, stdin_data: nil)
|
|
585
|
+
super(
|
|
586
|
+
argv: Immutable.strings(argv),
|
|
587
|
+
stdin_data: stdin_data.nil? ? nil : Immutable.string(stdin_data),
|
|
588
|
+
environment: Immutable.hash(environment),
|
|
589
|
+
credential_environment_keys:
|
|
590
|
+
Immutable.strings(credential_environment_keys),
|
|
591
|
+
session_id: Immutable.string(session_id),
|
|
592
|
+
message_id: Immutable.string(message_id)
|
|
593
|
+
)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def environment_for(env: ENV)
|
|
597
|
+
selected = credential_environment_keys.each_with_object({}) do |key, values|
|
|
598
|
+
value = env[key]
|
|
599
|
+
values[key] = value.to_s unless value.to_s.empty?
|
|
600
|
+
end
|
|
601
|
+
selected.merge(environment).freeze
|
|
602
|
+
end
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
NormalizedOutcome = Data.define(
|
|
606
|
+
:provider, :launcher_identity, :kind, :termination,
|
|
607
|
+
:final_message, :final_message_truncated, :identity, :usage, :diagnostic,
|
|
608
|
+
:unknown_events, :session_id, :message_id
|
|
609
|
+
) do
|
|
610
|
+
KINDS = %i[
|
|
611
|
+
completed authentication_failure configuration_failure cli_failure
|
|
612
|
+
malformed_output cancelled timed_out
|
|
613
|
+
].freeze
|
|
614
|
+
|
|
615
|
+
def initialize(provider:, launcher_identity:, kind:, termination:,
|
|
616
|
+
final_message: nil, identity:, usage: nil, diagnostic: nil,
|
|
617
|
+
unknown_events: [], session_id: nil, message_id: nil,
|
|
618
|
+
final_message_truncated: false)
|
|
619
|
+
normalized_kind = kind.to_sym
|
|
620
|
+
unless KINDS.include?(normalized_kind)
|
|
621
|
+
raise ArgumentError, "invalid normalized outcome kind"
|
|
622
|
+
end
|
|
623
|
+
unless termination.is_a?(TerminationEvidence)
|
|
624
|
+
raise ArgumentError, "termination must be TerminationEvidence"
|
|
625
|
+
end
|
|
626
|
+
unless identity.is_a?(RouteIdentity)
|
|
627
|
+
raise ArgumentError, "identity must be RouteIdentity"
|
|
628
|
+
end
|
|
629
|
+
unless usage.nil? || usage.is_a?(NormalizedUsage)
|
|
630
|
+
raise ArgumentError, "usage must be NormalizedUsage or nil"
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
super(
|
|
634
|
+
provider: provider.to_sym,
|
|
635
|
+
launcher_identity: Immutable.string(launcher_identity),
|
|
636
|
+
kind: normalized_kind,
|
|
637
|
+
termination: termination,
|
|
638
|
+
final_message:
|
|
639
|
+
final_message.nil? ? nil : Immutable.string(final_message),
|
|
640
|
+
final_message_truncated: final_message_truncated == true,
|
|
641
|
+
identity: identity,
|
|
642
|
+
usage: usage,
|
|
643
|
+
diagnostic: diagnostic.nil? ? nil : Immutable.string(diagnostic),
|
|
644
|
+
unknown_events: Immutable.strings(unknown_events),
|
|
645
|
+
session_id: session_id.nil? ? nil : Immutable.string(session_id),
|
|
646
|
+
message_id: message_id.nil? ? nil : Immutable.string(message_id)
|
|
647
|
+
)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def completed?
|
|
651
|
+
kind == :completed
|
|
652
|
+
end
|
|
653
|
+
end
|
|
176
654
|
end
|
data/lib/agent_cli_runtime.rb
CHANGED
|
@@ -8,10 +8,16 @@ 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"
|
|
12
|
+
require "agent_cli_runtime/opencode/result_parser"
|
|
11
13
|
require "agent_cli_runtime/profile"
|
|
12
14
|
require "agent_cli_runtime/profiles"
|
|
13
15
|
require "agent_cli_runtime/probe"
|
|
14
16
|
require "agent_cli_runtime/runtime"
|
|
17
|
+
require "agent_cli_runtime/opencode/probe"
|
|
18
|
+
require "agent_cli_runtime/opencode/permissions"
|
|
19
|
+
require "agent_cli_runtime/opencode/overlay"
|
|
20
|
+
require "agent_cli_runtime/opencode/inspection"
|
|
15
21
|
require "agent_cli_runtime/cli"
|
|
16
22
|
|
|
17
23
|
module AgentCliRuntime
|
|
@@ -21,8 +27,8 @@ module AgentCliRuntime
|
|
|
21
27
|
Runtime.compile(request)
|
|
22
28
|
end
|
|
23
29
|
|
|
24
|
-
def prepare!(profile)
|
|
25
|
-
Runtime.prepare!(profile)
|
|
30
|
+
def prepare!(profile, env: ENV)
|
|
31
|
+
Runtime.prepare!(profile, env:)
|
|
26
32
|
end
|
|
27
33
|
|
|
28
34
|
def require_capability!(profile, capability)
|
|
@@ -33,12 +39,32 @@ module AgentCliRuntime
|
|
|
33
39
|
Runtime.extract_usage(profile, event)
|
|
34
40
|
end
|
|
35
41
|
|
|
42
|
+
def extract_provider_error(profile, event)
|
|
43
|
+
Runtime.extract_provider_error(profile, event)
|
|
44
|
+
end
|
|
45
|
+
|
|
36
46
|
def observe(profile, result)
|
|
37
47
|
Runtime.observe(profile, result)
|
|
38
48
|
end
|
|
39
49
|
|
|
50
|
+
def parse_run(profile, stdout:)
|
|
51
|
+
Runtime.parse_run(profile, stdout:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def prepare_inspection(prepared, parsed_run)
|
|
55
|
+
OpenCode::Inspection.compile(prepared, parsed_run)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def normalize(profile, captured, requested_route:)
|
|
59
|
+
Runtime.normalize(profile, captured, requested_route:)
|
|
60
|
+
end
|
|
61
|
+
|
|
40
62
|
def probe(profile, home: nil, env: ENV)
|
|
41
|
-
|
|
63
|
+
if profile.is_a?(ProbeRequest)
|
|
64
|
+
OpenCode::Probe.call(profile, env: env)
|
|
65
|
+
else
|
|
66
|
+
Probe.call(profile, home: home, env: env)
|
|
67
|
+
end
|
|
42
68
|
end
|
|
43
69
|
|
|
44
70
|
def probe_all(home: nil, env: ENV)
|
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.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Kuznetsov
|
|
@@ -64,10 +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
|
-
|
|
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.
|
|
71
72
|
email:
|
|
72
73
|
- ivan@ikuznetsov.com
|
|
73
74
|
executables:
|
|
@@ -82,7 +83,13 @@ files:
|
|
|
82
83
|
- exe/agent-runtime
|
|
83
84
|
- lib/agent_cli_runtime.rb
|
|
84
85
|
- lib/agent_cli_runtime/cli.rb
|
|
86
|
+
- lib/agent_cli_runtime/error_extractors.rb
|
|
85
87
|
- lib/agent_cli_runtime/errors.rb
|
|
88
|
+
- lib/agent_cli_runtime/opencode/inspection.rb
|
|
89
|
+
- lib/agent_cli_runtime/opencode/overlay.rb
|
|
90
|
+
- lib/agent_cli_runtime/opencode/permissions.rb
|
|
91
|
+
- lib/agent_cli_runtime/opencode/probe.rb
|
|
92
|
+
- lib/agent_cli_runtime/opencode/result_parser.rb
|
|
86
93
|
- lib/agent_cli_runtime/probe.rb
|
|
87
94
|
- lib/agent_cli_runtime/profile.rb
|
|
88
95
|
- lib/agent_cli_runtime/profiles.rb
|
|
@@ -117,5 +124,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
117
124
|
requirements: []
|
|
118
125
|
rubygems_version: 3.6.9
|
|
119
126
|
specification_version: 4
|
|
120
|
-
summary:
|
|
127
|
+
summary: One Ruby API for Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode
|
|
121
128
|
test_files: []
|