agent-cli-runtime 0.1.0 → 0.2.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 +34 -0
- data/README.md +201 -41
- data/agent-cli-runtime.gemspec +7 -7
- 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 +573 -0
- data/lib/agent_cli_runtime/opencode/probe.rb +280 -0
- data/lib/agent_cli_runtime/opencode/result_parser.rb +402 -0
- data/lib/agent_cli_runtime/profile.rb +96 -2
- data/lib/agent_cli_runtime/profiles.rb +131 -1
- data/lib/agent_cli_runtime/runtime.rb +18 -3
- data/lib/agent_cli_runtime/values.rb +437 -3
- data/lib/agent_cli_runtime/version.rb +1 -1
- data/lib/agent_cli_runtime.rb +23 -3
- metadata +13 -8
|
@@ -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
|
|
|
@@ -154,10 +183,11 @@ module AgentCliRuntime
|
|
|
154
183
|
|
|
155
184
|
ObservableResult = Data.define(
|
|
156
185
|
:provider, :launcher_identity, :exit_code, :timed_out, :status,
|
|
157
|
-
:usage, :final_message, :diagnostic
|
|
186
|
+
:usage, :final_message, :diagnostic, :provider_signal
|
|
158
187
|
) do
|
|
159
188
|
def initialize(provider:, launcher_identity:, exit_code:, timed_out:,
|
|
160
|
-
status:, usage:, final_message:, diagnostic
|
|
189
|
+
status:, usage:, final_message:, diagnostic:,
|
|
190
|
+
provider_signal: nil)
|
|
161
191
|
super(
|
|
162
192
|
provider: provider.to_sym,
|
|
163
193
|
launcher_identity: Immutable.string(launcher_identity),
|
|
@@ -167,8 +197,412 @@ module AgentCliRuntime
|
|
|
167
197
|
usage: usage&.dup&.freeze,
|
|
168
198
|
final_message:
|
|
169
199
|
final_message.nil? ? nil : Immutable.string(final_message),
|
|
170
|
-
diagnostic: diagnostic.nil? ? nil : Immutable.string(diagnostic)
|
|
200
|
+
diagnostic: diagnostic.nil? ? nil : Immutable.string(diagnostic),
|
|
201
|
+
provider_signal: provider_signal&.dup&.freeze
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
Route = Data.define(:provider, :model) do
|
|
208
|
+
ROUTE_PATTERN = /\A(?<provider>[a-zA-Z0-9][a-zA-Z0-9._-]*)\/(?<model>[^\s\/][^\s]*)\z/
|
|
209
|
+
|
|
210
|
+
def initialize(provider:, model:)
|
|
211
|
+
normalized_provider = Immutable.string(provider)
|
|
212
|
+
normalized_model = Immutable.string(model)
|
|
213
|
+
route = "#{normalized_provider}/#{normalized_model}"
|
|
214
|
+
unless ROUTE_PATTERN.match?(route) && !normalized_model.include?("\0")
|
|
215
|
+
raise ArgumentError,
|
|
216
|
+
"OpenCode route must be a full provider/model value"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
super(provider: normalized_provider, model: normalized_model)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.parse(value)
|
|
223
|
+
match = ROUTE_PATTERN.match(value.to_s)
|
|
224
|
+
unless match
|
|
225
|
+
raise ArgumentError,
|
|
226
|
+
"OpenCode route must be a full provider/model value"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
new(provider: match[:provider], model: match[:model])
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def to_s
|
|
233
|
+
"#{provider}/#{model}"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
OpenCodePermissionPolicy = Data.define(:rules) do
|
|
238
|
+
ACTIONS = %w[allow ask deny].freeze
|
|
239
|
+
|
|
240
|
+
def initialize(rules = nil, **keywords)
|
|
241
|
+
value = rules || keywords[:rules] || keywords
|
|
242
|
+
normalized = Immutable.hash(value || {})
|
|
243
|
+
raise ArgumentError, "OpenCode permission policy cannot be empty" if normalized.empty?
|
|
244
|
+
|
|
245
|
+
validate_actions!(normalized)
|
|
246
|
+
super(rules: normalized)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
private
|
|
250
|
+
|
|
251
|
+
def validate_actions!(value)
|
|
252
|
+
value.each_value do |entry|
|
|
253
|
+
if entry.is_a?(Hash)
|
|
254
|
+
validate_actions!(entry)
|
|
255
|
+
elsif !ACTIONS.include?(entry)
|
|
256
|
+
raise ArgumentError,
|
|
257
|
+
"OpenCode permission action must be allow, ask, or deny"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
OpenCodePreparationRequest = Data.define(
|
|
264
|
+
:request, :working_directory, :invocation_root,
|
|
265
|
+
:configuration_path, :configuration,
|
|
266
|
+
:credential_environment_keys, :credential_file,
|
|
267
|
+
:permission_policy, :additional_read_roots,
|
|
268
|
+
:additional_write_roots, :edit_patterns, :plugins, :pure
|
|
269
|
+
) do
|
|
270
|
+
def initialize(request:, working_directory:, invocation_root:,
|
|
271
|
+
configuration_path: nil, configuration: nil,
|
|
272
|
+
credential_environment_keys: [], credential_file: nil,
|
|
273
|
+
permission_policy: nil, additional_read_roots: [],
|
|
274
|
+
additional_write_roots: [], edit_patterns: [], plugins: [], pure: true)
|
|
275
|
+
unless request.is_a?(Request)
|
|
276
|
+
raise ArgumentError, "request must be an AgentCliRuntime::Request"
|
|
277
|
+
end
|
|
278
|
+
if configuration_path && configuration
|
|
279
|
+
raise ArgumentError,
|
|
280
|
+
"choose configuration_path or configuration, not both"
|
|
281
|
+
end
|
|
282
|
+
if permission_policy &&
|
|
283
|
+
!permission_policy.is_a?(OpenCodePermissionPolicy)
|
|
284
|
+
raise ArgumentError,
|
|
285
|
+
"permission_policy must be an OpenCodePermissionPolicy"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
keys = Immutable.strings(credential_environment_keys)
|
|
289
|
+
invalid = keys.find { |key| !key.match?(/\A[A-Z][A-Z0-9_]*\z/) }
|
|
290
|
+
raise ArgumentError, "invalid credential environment key" if invalid
|
|
291
|
+
raise ArgumentError, "credential environment keys must be unique" if keys.uniq.length != keys.length
|
|
292
|
+
reserved = keys & OPENCODE_OVERLAY_ENVIRONMENT_KEYS
|
|
293
|
+
unless reserved.empty?
|
|
294
|
+
raise ArgumentError,
|
|
295
|
+
"credential environment keys cannot override the OpenCode overlay: #{reserved.join(', ')}"
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
super(
|
|
299
|
+
request: request,
|
|
300
|
+
working_directory: Immutable.string(working_directory),
|
|
301
|
+
invocation_root: Immutable.string(invocation_root),
|
|
302
|
+
configuration_path:
|
|
303
|
+
configuration_path.nil? ? nil : Immutable.string(configuration_path),
|
|
304
|
+
configuration:
|
|
305
|
+
configuration.nil? ? nil : Immutable.hash(configuration),
|
|
306
|
+
credential_environment_keys: keys,
|
|
307
|
+
credential_file:
|
|
308
|
+
credential_file.nil? ? nil : Immutable.string(credential_file),
|
|
309
|
+
permission_policy: permission_policy,
|
|
310
|
+
additional_read_roots: Immutable.strings(additional_read_roots),
|
|
311
|
+
additional_write_roots: Immutable.strings(additional_write_roots),
|
|
312
|
+
edit_patterns: Immutable.strings(edit_patterns),
|
|
313
|
+
plugins: Immutable.strings(plugins),
|
|
314
|
+
pure: pure != false
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
ProbeRequest = Data.define(
|
|
320
|
+
:profile, :route, :variant, :environment,
|
|
321
|
+
:credential_environment_keys, :credential_file_staged
|
|
322
|
+
) do
|
|
323
|
+
def initialize(profile:, route:, variant: nil, environment: {},
|
|
324
|
+
credential_environment_keys: [],
|
|
325
|
+
credential_file_staged: false)
|
|
326
|
+
parsed_route = route.is_a?(Route) ? route : Route.parse(route)
|
|
327
|
+
super(
|
|
328
|
+
profile: profile,
|
|
329
|
+
route: parsed_route,
|
|
330
|
+
variant: variant.nil? ? nil : Immutable.string(variant),
|
|
331
|
+
environment: Immutable.hash(environment),
|
|
332
|
+
credential_environment_keys:
|
|
333
|
+
Immutable.strings(credential_environment_keys),
|
|
334
|
+
credential_file_staged: credential_file_staged == true
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
RouteProbeResult = Data.define(
|
|
340
|
+
:provider, :ready, :installed, :executable, :version,
|
|
341
|
+
:minimum_version, :auth_configuration, :route,
|
|
342
|
+
:route_available, :available_variants,
|
|
343
|
+
:capability_evidence, :diagnostic
|
|
344
|
+
) do
|
|
345
|
+
def initialize(provider:, ready:, installed:, executable:, version:,
|
|
346
|
+
minimum_version:, auth_configuration:, route:,
|
|
347
|
+
route_available:, available_variants:,
|
|
348
|
+
capability_evidence:, diagnostic: nil)
|
|
349
|
+
super(
|
|
350
|
+
provider: provider.to_sym,
|
|
351
|
+
ready: ready == true,
|
|
352
|
+
installed: installed == true,
|
|
353
|
+
executable: Immutable.string(executable),
|
|
354
|
+
version: version.nil? ? nil : Immutable.string(version),
|
|
355
|
+
minimum_version:
|
|
356
|
+
minimum_version.nil? ? nil : Immutable.string(minimum_version),
|
|
357
|
+
auth_configuration: auth_configuration,
|
|
358
|
+
route: route,
|
|
359
|
+
route_available: route_available == true,
|
|
360
|
+
available_variants: Immutable.strings(available_variants),
|
|
361
|
+
capability_evidence: Array(capability_evidence).freeze,
|
|
362
|
+
diagnostic:
|
|
363
|
+
diagnostic.nil? ? nil : Immutable.string(diagnostic)
|
|
364
|
+
)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
class PreparedInvocation
|
|
369
|
+
attr_reader :invocation, :environment, :credential_environment_keys,
|
|
370
|
+
:invocation_root, :generated_paths, :configuration_path,
|
|
371
|
+
:requested_route, :configuration_source, :probe_result,
|
|
372
|
+
:executable
|
|
373
|
+
|
|
374
|
+
def initialize(invocation:, environment:, credential_environment_keys:,
|
|
375
|
+
invocation_root:, generated_paths:, configuration_path:,
|
|
376
|
+
requested_route:, configuration_source:, probe_result:,
|
|
377
|
+
cleanup:, executable: nil)
|
|
378
|
+
@invocation = invocation
|
|
379
|
+
@environment = Immutable.hash(environment)
|
|
380
|
+
@credential_environment_keys =
|
|
381
|
+
Immutable.strings(credential_environment_keys)
|
|
382
|
+
@invocation_root = Immutable.string(invocation_root)
|
|
383
|
+
@generated_paths = Immutable.strings(generated_paths)
|
|
384
|
+
@configuration_path = Immutable.string(configuration_path)
|
|
385
|
+
@requested_route = requested_route
|
|
386
|
+
@configuration_source =
|
|
387
|
+
configuration_source.nil? ? nil : Immutable.string(configuration_source)
|
|
388
|
+
@probe_result = probe_result
|
|
389
|
+
@executable = Immutable.string(executable || invocation.argv.fetch(0))
|
|
390
|
+
@cleanup = cleanup
|
|
391
|
+
freeze
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def environment_for(env: ENV)
|
|
395
|
+
selected = credential_environment_keys.each_with_object({}) do |key, values|
|
|
396
|
+
value = env[key]
|
|
397
|
+
values[key] = value.to_s unless value.to_s.empty?
|
|
398
|
+
end
|
|
399
|
+
selected.merge(environment).freeze
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def cleanup!
|
|
403
|
+
@cleanup.call
|
|
404
|
+
nil
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
TerminationEvidence = Data.define(
|
|
409
|
+
:exit_code, :timed_out, :cancelled, :signal
|
|
410
|
+
) do
|
|
411
|
+
def initialize(exit_code:, timed_out: false, cancelled: false, signal: nil)
|
|
412
|
+
unless exit_code.nil? || exit_code.is_a?(Integer)
|
|
413
|
+
raise ArgumentError, "exit_code must be an Integer or nil"
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
super(
|
|
417
|
+
exit_code: exit_code,
|
|
418
|
+
timed_out: timed_out == true,
|
|
419
|
+
cancelled: cancelled == true,
|
|
420
|
+
signal: signal.nil? ? nil : Immutable.string(signal)
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def success?
|
|
425
|
+
!timed_out && !cancelled && signal.nil? && exit_code == 0
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
CapturedResult = Data.define(
|
|
430
|
+
:stdout, :stderr, :termination, :inspection_output
|
|
431
|
+
) do
|
|
432
|
+
def initialize(stdout:, stderr:, termination:, inspection_output: nil)
|
|
433
|
+
unless termination.is_a?(TerminationEvidence)
|
|
434
|
+
raise ArgumentError, "termination must be TerminationEvidence"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
super(
|
|
438
|
+
stdout: Immutable.string(stdout),
|
|
439
|
+
stderr: Immutable.string(stderr),
|
|
440
|
+
termination: termination,
|
|
441
|
+
inspection_output:
|
|
442
|
+
inspection_output.nil? ? nil : Immutable.string(inspection_output)
|
|
443
|
+
)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
NormalizedUsage = Data.define(
|
|
448
|
+
:input, :output, :cache_read, :cache_write, :reasoning, :cost
|
|
449
|
+
) do
|
|
450
|
+
def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil,
|
|
451
|
+
reasoning: nil, cost: nil)
|
|
452
|
+
super(
|
|
453
|
+
input: number(input, :input, integer: true),
|
|
454
|
+
output: number(output, :output, integer: true),
|
|
455
|
+
cache_read: number(cache_read, :cache_read, integer: true),
|
|
456
|
+
cache_write: number(cache_write, :cache_write, integer: true),
|
|
457
|
+
reasoning: number(reasoning, :reasoning, integer: true),
|
|
458
|
+
cost: number(cost, :cost, integer: false)
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def cached
|
|
463
|
+
return nil if cache_read.nil? || cache_write.nil?
|
|
464
|
+
|
|
465
|
+
cache_read + cache_write
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
private
|
|
469
|
+
|
|
470
|
+
def number(value, label, integer:)
|
|
471
|
+
return nil if value.nil?
|
|
472
|
+
valid = integer ? value.is_a?(Integer) : value.is_a?(Numeric)
|
|
473
|
+
valid &&= value.finite? if value.respond_to?(:finite?)
|
|
474
|
+
unless valid && value >= 0
|
|
475
|
+
raise ArgumentError, "#{label} must be a non-negative number or nil"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
value
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
RouteIdentity = Data.define(:requested, :actual, :resolution_status) do
|
|
483
|
+
RESOLUTION_STATUSES = %i[unobserved matched resolved_differently].freeze
|
|
484
|
+
|
|
485
|
+
def initialize(requested:, actual: nil, resolution_status: nil)
|
|
486
|
+
requested_route = requested.is_a?(Route) ? requested : Route.parse(requested)
|
|
487
|
+
actual_route =
|
|
488
|
+
if actual.nil?
|
|
489
|
+
nil
|
|
490
|
+
elsif actual.is_a?(Route)
|
|
491
|
+
actual
|
|
492
|
+
else
|
|
493
|
+
Route.parse(actual)
|
|
494
|
+
end
|
|
495
|
+
status = resolution_status&.to_sym ||
|
|
496
|
+
(actual_route.nil? ? :unobserved :
|
|
497
|
+
(requested_route == actual_route ? :matched : :resolved_differently))
|
|
498
|
+
unless RESOLUTION_STATUSES.include?(status)
|
|
499
|
+
raise ArgumentError, "invalid route resolution status"
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
super(
|
|
503
|
+
requested: requested_route,
|
|
504
|
+
actual: actual_route,
|
|
505
|
+
resolution_status: status
|
|
506
|
+
)
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
ParsedRun = Data.define(
|
|
511
|
+
:session_id, :terminal_message_id, :terminal_reason,
|
|
512
|
+
:final_message, :final_message_truncated, :preliminary_usage, :unknown_events
|
|
513
|
+
) do
|
|
514
|
+
def initialize(session_id:, terminal_message_id:, terminal_reason:,
|
|
515
|
+
final_message:, preliminary_usage:, unknown_events: [],
|
|
516
|
+
final_message_truncated: false)
|
|
517
|
+
unless preliminary_usage.is_a?(NormalizedUsage)
|
|
518
|
+
raise ArgumentError, "preliminary_usage must be NormalizedUsage"
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
super(
|
|
522
|
+
session_id: Immutable.string(session_id),
|
|
523
|
+
terminal_message_id: Immutable.string(terminal_message_id),
|
|
524
|
+
terminal_reason: Immutable.string(terminal_reason),
|
|
525
|
+
final_message: Immutable.string(final_message),
|
|
526
|
+
final_message_truncated: final_message_truncated == true,
|
|
527
|
+
preliminary_usage: preliminary_usage,
|
|
528
|
+
unknown_events: Immutable.strings(unknown_events)
|
|
529
|
+
)
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
InspectionCommand = Data.define(
|
|
534
|
+
:argv, :stdin_data, :environment, :credential_environment_keys,
|
|
535
|
+
:session_id, :message_id
|
|
536
|
+
) do
|
|
537
|
+
def initialize(argv:, environment:, credential_environment_keys:,
|
|
538
|
+
session_id:, message_id:, stdin_data: nil)
|
|
539
|
+
super(
|
|
540
|
+
argv: Immutable.strings(argv),
|
|
541
|
+
stdin_data: stdin_data.nil? ? nil : Immutable.string(stdin_data),
|
|
542
|
+
environment: Immutable.hash(environment),
|
|
543
|
+
credential_environment_keys:
|
|
544
|
+
Immutable.strings(credential_environment_keys),
|
|
545
|
+
session_id: Immutable.string(session_id),
|
|
546
|
+
message_id: Immutable.string(message_id)
|
|
547
|
+
)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def environment_for(env: ENV)
|
|
551
|
+
selected = credential_environment_keys.each_with_object({}) do |key, values|
|
|
552
|
+
value = env[key]
|
|
553
|
+
values[key] = value.to_s unless value.to_s.empty?
|
|
554
|
+
end
|
|
555
|
+
selected.merge(environment).freeze
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
NormalizedOutcome = Data.define(
|
|
560
|
+
:provider, :launcher_identity, :kind, :termination,
|
|
561
|
+
:final_message, :final_message_truncated, :identity, :usage, :diagnostic,
|
|
562
|
+
:unknown_events, :session_id, :message_id
|
|
563
|
+
) do
|
|
564
|
+
KINDS = %i[
|
|
565
|
+
completed authentication_failure configuration_failure cli_failure
|
|
566
|
+
malformed_output cancelled timed_out
|
|
567
|
+
].freeze
|
|
568
|
+
|
|
569
|
+
def initialize(provider:, launcher_identity:, kind:, termination:,
|
|
570
|
+
final_message: nil, identity:, usage: nil, diagnostic: nil,
|
|
571
|
+
unknown_events: [], session_id: nil, message_id: nil,
|
|
572
|
+
final_message_truncated: false)
|
|
573
|
+
normalized_kind = kind.to_sym
|
|
574
|
+
unless KINDS.include?(normalized_kind)
|
|
575
|
+
raise ArgumentError, "invalid normalized outcome kind"
|
|
576
|
+
end
|
|
577
|
+
unless termination.is_a?(TerminationEvidence)
|
|
578
|
+
raise ArgumentError, "termination must be TerminationEvidence"
|
|
579
|
+
end
|
|
580
|
+
unless identity.is_a?(RouteIdentity)
|
|
581
|
+
raise ArgumentError, "identity must be RouteIdentity"
|
|
582
|
+
end
|
|
583
|
+
unless usage.nil? || usage.is_a?(NormalizedUsage)
|
|
584
|
+
raise ArgumentError, "usage must be NormalizedUsage or nil"
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
super(
|
|
588
|
+
provider: provider.to_sym,
|
|
589
|
+
launcher_identity: Immutable.string(launcher_identity),
|
|
590
|
+
kind: normalized_kind,
|
|
591
|
+
termination: termination,
|
|
592
|
+
final_message:
|
|
593
|
+
final_message.nil? ? nil : Immutable.string(final_message),
|
|
594
|
+
final_message_truncated: final_message_truncated == true,
|
|
595
|
+
identity: identity,
|
|
596
|
+
usage: usage,
|
|
597
|
+
diagnostic: diagnostic.nil? ? nil : Immutable.string(diagnostic),
|
|
598
|
+
unknown_events: Immutable.strings(unknown_events),
|
|
599
|
+
session_id: session_id.nil? ? nil : Immutable.string(session_id),
|
|
600
|
+
message_id: message_id.nil? ? nil : Immutable.string(message_id)
|
|
171
601
|
)
|
|
172
602
|
end
|
|
603
|
+
|
|
604
|
+
def completed?
|
|
605
|
+
kind == :completed
|
|
606
|
+
end
|
|
173
607
|
end
|
|
174
608
|
end
|
data/lib/agent_cli_runtime.rb
CHANGED
|
@@ -8,10 +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/opencode/result_parser"
|
|
11
12
|
require "agent_cli_runtime/profile"
|
|
12
13
|
require "agent_cli_runtime/profiles"
|
|
13
14
|
require "agent_cli_runtime/probe"
|
|
14
15
|
require "agent_cli_runtime/runtime"
|
|
16
|
+
require "agent_cli_runtime/opencode/probe"
|
|
17
|
+
require "agent_cli_runtime/opencode/overlay"
|
|
18
|
+
require "agent_cli_runtime/opencode/inspection"
|
|
15
19
|
require "agent_cli_runtime/cli"
|
|
16
20
|
|
|
17
21
|
module AgentCliRuntime
|
|
@@ -21,8 +25,8 @@ module AgentCliRuntime
|
|
|
21
25
|
Runtime.compile(request)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
|
-
def prepare!(profile)
|
|
25
|
-
Runtime.prepare!(profile)
|
|
28
|
+
def prepare!(profile, env: ENV)
|
|
29
|
+
Runtime.prepare!(profile, env:)
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
def require_capability!(profile, capability)
|
|
@@ -37,8 +41,24 @@ module AgentCliRuntime
|
|
|
37
41
|
Runtime.observe(profile, result)
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
def parse_run(profile, stdout:)
|
|
45
|
+
Runtime.parse_run(profile, stdout:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def prepare_inspection(prepared, parsed_run)
|
|
49
|
+
OpenCode::Inspection.compile(prepared, parsed_run)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def normalize(profile, captured, requested_route:)
|
|
53
|
+
Runtime.normalize(profile, captured, requested_route:)
|
|
54
|
+
end
|
|
55
|
+
|
|
40
56
|
def probe(profile, home: nil, env: ENV)
|
|
41
|
-
|
|
57
|
+
if profile.is_a?(ProbeRequest)
|
|
58
|
+
OpenCode::Probe.call(profile, env: env)
|
|
59
|
+
else
|
|
60
|
+
Probe.call(profile, home: home, env: env)
|
|
61
|
+
end
|
|
42
62
|
end
|
|
43
63
|
|
|
44
64
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ivan Kuznetsov
|
|
@@ -66,8 +66,9 @@ dependencies:
|
|
|
66
66
|
description: |
|
|
67
67
|
Agent CLI Runtime provides immutable profiles, invocation compilation,
|
|
68
68
|
local prerequisite probes, capability evidence, usage extraction, and
|
|
69
|
-
result normalization for Claude Code, Codex CLI, Pi,
|
|
70
|
-
not spawn agents or claim live provider health, quota,
|
|
69
|
+
result normalization for Claude Code, Codex CLI, Pi, Grok CLI, and
|
|
70
|
+
OpenCode. It does not spawn agents or claim live provider health, quota,
|
|
71
|
+
or credential validity.
|
|
71
72
|
email:
|
|
72
73
|
- ivan@ikuznetsov.com
|
|
73
74
|
executables:
|
|
@@ -83,6 +84,10 @@ files:
|
|
|
83
84
|
- lib/agent_cli_runtime.rb
|
|
84
85
|
- lib/agent_cli_runtime/cli.rb
|
|
85
86
|
- lib/agent_cli_runtime/errors.rb
|
|
87
|
+
- lib/agent_cli_runtime/opencode/inspection.rb
|
|
88
|
+
- lib/agent_cli_runtime/opencode/overlay.rb
|
|
89
|
+
- lib/agent_cli_runtime/opencode/probe.rb
|
|
90
|
+
- lib/agent_cli_runtime/opencode/result_parser.rb
|
|
86
91
|
- lib/agent_cli_runtime/probe.rb
|
|
87
92
|
- lib/agent_cli_runtime/profile.rb
|
|
88
93
|
- lib/agent_cli_runtime/profiles.rb
|
|
@@ -91,15 +96,15 @@ files:
|
|
|
91
96
|
- lib/agent_cli_runtime/usage_extractors.rb
|
|
92
97
|
- lib/agent_cli_runtime/values.rb
|
|
93
98
|
- lib/agent_cli_runtime/version.rb
|
|
94
|
-
homepage: https://github.com/ivankuznetsov/
|
|
99
|
+
homepage: https://github.com/ivankuznetsov/agent-cli-runtime
|
|
95
100
|
licenses:
|
|
96
101
|
- MIT
|
|
97
102
|
metadata:
|
|
98
|
-
homepage_uri: https://github.com/ivankuznetsov/
|
|
99
|
-
source_code_uri: https://github.com/ivankuznetsov/
|
|
100
|
-
changelog_uri: https://github.com/ivankuznetsov/
|
|
103
|
+
homepage_uri: https://github.com/ivankuznetsov/agent-cli-runtime
|
|
104
|
+
source_code_uri: https://github.com/ivankuznetsov/agent-cli-runtime
|
|
105
|
+
changelog_uri: https://github.com/ivankuznetsov/agent-cli-runtime/blob/main/CHANGELOG.md
|
|
101
106
|
bug_tracker_uri: https://github.com/ivankuznetsov/hive/issues
|
|
102
|
-
documentation_uri: https://github.com/ivankuznetsov/
|
|
107
|
+
documentation_uri: https://github.com/ivankuznetsov/agent-cli-runtime/blob/main/README.md
|
|
103
108
|
rubygems_mfa_required: 'true'
|
|
104
109
|
rdoc_options: []
|
|
105
110
|
require_paths:
|