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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0202bb311856ad96fcd486517d11cfc7175db0102a188920c4d851bf338b7d6
|
|
4
|
+
data.tar.gz: 2e72a23b5e7a021fc439a9ce8ef34bf166c0af0141de491a27686d6f3ba3f139
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3736f38aa29d809028b1e99c9df1dd17ddb4a210382e89f752def380e84cf22e3f144da2d10ee6c86c2dab8496865a35347196277367a0734b9c39991c33f0e
|
|
7
|
+
data.tar.gz: 5adacccc362958a8cc9a2eeab22b9100041101de4795d7f31ddaa563406ec031f6b3662e84eca058b144ab86ee41d1505c42baa267c1eae436cda4566680b100
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.4 - 2026-08-25
|
|
6
|
+
|
|
7
|
+
- Expose the OpenCode permission compiler independently from the prepared
|
|
8
|
+
overlay so callers can supply the same deny-first rules through native
|
|
9
|
+
per-process configuration without duplicating policy generation.
|
|
10
|
+
|
|
11
|
+
- Carry the already-resolved OpenCode executable on `ProbeRequest` (`executable:`)
|
|
12
|
+
instead of re-encoding it as the `AGENT_CLI_RUNTIME_OPENCODE_BIN` environment
|
|
13
|
+
override. The route-aware probe now resolves the executable once — honoring a
|
|
14
|
+
caller-supplied value first — and threads that single value through the
|
|
15
|
+
installation check, version check, every local inspection command, and the
|
|
16
|
+
reported result executable. `Profile#binary_installed?`,
|
|
17
|
+
`Profile#check_version!`, and `Profile#capture_local` accept an optional
|
|
18
|
+
`executable:` override; existing environment-based resolution is unchanged
|
|
19
|
+
when no explicit value is carried.
|
|
20
|
+
|
|
21
|
+
## 0.2.3 - 2026-08-21
|
|
22
|
+
|
|
23
|
+
- Classify extracted provider failures once at the runtime boundary as
|
|
24
|
+
`provider_limit`, `rate_limited`, `model_output_limit`, or `provider_error`.
|
|
25
|
+
HTTP 402/429 payloads are authoritative even when their provider-specific
|
|
26
|
+
wording does not match an orchestrator's legacy quota phrases.
|
|
27
|
+
|
|
28
|
+
## 0.2.2 - 2026-08-18
|
|
29
|
+
|
|
30
|
+
- Deliver built-in Pi prompts through its native non-TTY stdin reader instead
|
|
31
|
+
of one positional argv element. This prevents implementation-sized prompts
|
|
32
|
+
from failing at process spawn with the operating system's per-argument
|
|
33
|
+
`E2BIG` limit; Codex retains its separate stdin-plus-`-` transport.
|
|
34
|
+
- Treat Pi's `stopReason: "length"` as a typed `model_output_limit` failure.
|
|
35
|
+
Pi exits zero after this provider stop even when the model exhausted its
|
|
36
|
+
output allowance before writing the requested artifact; callers can now
|
|
37
|
+
distinguish that incomplete turn from an agent that silently produced
|
|
38
|
+
0 bytes and tell operators to raise `maxTokens` or lower reasoning effort.
|
|
39
|
+
- Declare Grok's filesystem sandbox flags, so a caller asking for confined
|
|
40
|
+
execution gets it instead of being told Grok cannot confine. `--sandbox
|
|
41
|
+
workspace` limits writes to the working directory and `--sandbox read-only`
|
|
42
|
+
forbids them, both built-in profiles that custom ones extend from
|
|
43
|
+
`~/.grok/sandbox.toml`; `--always-approve` suppresses the approval prompt a
|
|
44
|
+
headless run can never answer, leaving the sandbox — not the prompt — as the
|
|
45
|
+
boundary. Only the Grok profile changes.
|
|
46
|
+
|
|
47
|
+
## 0.2.1 - 2026-08-17
|
|
48
|
+
|
|
49
|
+
- Add per-profile provider-error extraction so a refusal a CLI reports on its
|
|
50
|
+
event stream is distinguishable from an agent that genuinely produced
|
|
51
|
+
nothing. `extract_provider_error` returns the provider, the HTTP status when
|
|
52
|
+
the provider supplied one, and the redacted provider text.
|
|
53
|
+
- Add the `pi` extractor for turns that keep the envelope type and carry the
|
|
54
|
+
refusal in `stopReason`/`errorMessage`, which no event-type match observes.
|
|
55
|
+
Such turns previously reached the caller as a clean run with empty content
|
|
56
|
+
while the process exited zero.
|
|
57
|
+
- Default every other profile to the previously assumed shapes — dedicated
|
|
58
|
+
`error`, `turn.failed`, and `rate_limit_event` events plus failed `result`
|
|
59
|
+
events — so no existing profile changes behaviour.
|
|
60
|
+
- Read usage from the assistant message and accept the bare
|
|
61
|
+
`input`/`output`/`cacheRead`/`cacheWrite` spellings, so a provider reporting
|
|
62
|
+
usage there is metered instead of silently recording nothing. The bare
|
|
63
|
+
spellings are matched last, leaving a provider that reports explicit
|
|
64
|
+
`*_tokens` keys with its existing reading.
|
|
65
|
+
|
|
66
|
+
## 0.2.0 - 2026-08-15
|
|
67
|
+
|
|
68
|
+
- Add first-class OpenCode `1.18.16+` support alongside the built-in Claude
|
|
69
|
+
Code, Codex CLI, Pi, and Grok CLI profiles. OpenCode uses the shared request
|
|
70
|
+
and facade, then adds provider-specific preparation, route probing, and
|
|
71
|
+
strict result normalization.
|
|
72
|
+
- Route every OpenCode run to an exact `provider/model` and validate model
|
|
73
|
+
variants, preserving the route the application requested and the route the
|
|
74
|
+
captured result reports.
|
|
75
|
+
- Prepare owner-private OpenCode config, data, cache, and state overlays for
|
|
76
|
+
each invocation, with scoped `read-only` and `workspace-write` policies,
|
|
77
|
+
deliberate credential forwarding, and idempotent cleanup.
|
|
78
|
+
- Check run/export capabilities, selected authentication source, cached model
|
|
79
|
+
inventory, and the exact requested route locally before an application
|
|
80
|
+
starts work.
|
|
81
|
+
- Correlate run and sanitized-export evidence into typed outcomes for
|
|
82
|
+
completion, authentication, configuration, CLI failure, malformed output,
|
|
83
|
+
cancellation, and timeout.
|
|
84
|
+
- Report input, output, cache-read, cache-write, reasoning, and cost evidence
|
|
85
|
+
while preserving the difference between unavailable data and numeric zero.
|
|
86
|
+
- Return prepared commands and normalized evidence through lifecycle-safe
|
|
87
|
+
values that fit an application's existing streaming, timeout, cancellation,
|
|
88
|
+
retry, and process-supervision stack.
|
|
89
|
+
|
|
3
90
|
## 0.1.1 - 2026-08-11
|
|
4
91
|
|
|
5
92
|
- Expose each profile's immutable credential-environment key inventory so
|
data/README.md
CHANGED
|
@@ -1,22 +1,43 @@
|
|
|
1
1
|
# Agent CLI Runtime
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Agent CLI Runtime gives Ruby applications one stable integration layer for
|
|
4
|
+
Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode. It builds provider-specific
|
|
5
|
+
commands from a shared request model, checks local versions and named
|
|
6
|
+
capabilities, and normalizes usage and results afterward. Centralized profiles,
|
|
7
|
+
environment rules, and parsers make agent providers easier to add, switch, and
|
|
8
|
+
upgrade.
|
|
9
|
+
|
|
10
|
+
Version 0.2.0 adds first-class OpenCode support with exact `provider/model`
|
|
11
|
+
routing, isolated per-invocation configuration, scoped permission policies,
|
|
12
|
+
offline readiness checks, and typed outcome normalization. See the
|
|
13
|
+
[changelog](CHANGELOG.md) for the complete release notes.
|
|
14
|
+
|
|
15
|
+
## Why use it?
|
|
16
|
+
|
|
17
|
+
Agent CLIs disagree on flags, prompt transport, configuration locations,
|
|
18
|
+
permission controls, usage events, and result formats. Agent CLI Runtime keeps
|
|
19
|
+
those differences in versioned profiles so application code can use one
|
|
20
|
+
request and result vocabulary.
|
|
21
|
+
|
|
22
|
+
- Add or switch agent CLIs without spreading provider conditionals throughout
|
|
23
|
+
the application.
|
|
24
|
+
- Validate installed versions, capabilities, and exact routes before starting
|
|
25
|
+
work.
|
|
26
|
+
- Preserve typed usage and outcome evidence across provider-specific output
|
|
27
|
+
formats.
|
|
28
|
+
- Add custom profiles through the same immutable compatibility contract.
|
|
8
29
|
|
|
9
30
|
## Install
|
|
10
31
|
|
|
11
32
|
```ruby
|
|
12
|
-
gem "agent-cli-runtime", "~> 0.
|
|
33
|
+
gem "agent-cli-runtime", "~> 0.2.0"
|
|
13
34
|
```
|
|
14
35
|
|
|
15
36
|
```ruby
|
|
16
37
|
require "agent_cli_runtime"
|
|
17
38
|
```
|
|
18
39
|
|
|
19
|
-
Ruby 3.4 or newer is required.
|
|
40
|
+
Ruby 3.4 or newer is required. The 0.2.x line is tested on Linux and macOS.
|
|
20
41
|
|
|
21
42
|
## Compile an invocation
|
|
22
43
|
|
|
@@ -39,6 +60,11 @@ Compilation does not execute the returned command. Unsupported requested
|
|
|
39
60
|
controls raise `AgentCliRuntime::UnsupportedCapability` with typed evidence
|
|
40
61
|
instead of silently widening the request.
|
|
41
62
|
|
|
63
|
+
Prompt transport is profile-owned. `:stdin` writes the prompt to stdin and
|
|
64
|
+
adds the CLI's conventional `-` argv marker; `:piped_stdin` writes the prompt
|
|
65
|
+
to stdin without a marker for CLIs such as Pi that consume a non-TTY stream
|
|
66
|
+
directly. Built-in Pi therefore keeps arbitrarily large prompts out of argv.
|
|
67
|
+
|
|
42
68
|
`permission_mode: nil` selects the profile's default non-interactive permission
|
|
43
69
|
flags, which may include a provider's bypass flag. Pass `"read-only"` or
|
|
44
70
|
`"workspace-write"` explicitly when the integration requires that constraint.
|
|
@@ -56,6 +82,16 @@ represent.
|
|
|
56
82
|
- `extract_usage(profile, event)` normalizes provider usage when present and
|
|
57
83
|
returns `nil` when usage is absent or malformed.
|
|
58
84
|
- `observe(profile, result)` normalizes bounded, redacted result metadata.
|
|
85
|
+
- `prepare!(open_code_preparation)` returns a `PreparedInvocation` containing
|
|
86
|
+
an isolated OpenCode overlay, argv, and child environment.
|
|
87
|
+
- `OpenCode::Permissions.compile(...)` returns the same deny-first permission
|
|
88
|
+
document independently for callers that retain native OpenCode state.
|
|
89
|
+
- `parse_run(profile, stdout:)` parses a successful OpenCode JSONL capture
|
|
90
|
+
into the session and terminal-message identity required for inspection.
|
|
91
|
+
- `prepare_inspection(prepared, parsed_run)` returns the non-model sanitized
|
|
92
|
+
session-export invocation.
|
|
93
|
+
- `normalize(profile, captured, requested_route:)` returns one typed OpenCode
|
|
94
|
+
outcome from caller-captured run, termination, and inspection evidence.
|
|
59
95
|
|
|
60
96
|
Provider arguments accept a built-in name or an `AgentCliRuntime::Profile`.
|
|
61
97
|
Unknown built-in names raise `AgentCliRuntime::UnknownProvider`; they are not
|
|
@@ -108,6 +144,138 @@ where the CLI owns its subscription/session state. `configuration_directory`
|
|
|
108
144
|
resolves that location from a caller-supplied home and environment without
|
|
109
145
|
reading credentials or deciding authentication policy.
|
|
110
146
|
|
|
147
|
+
## Prepare and normalize OpenCode
|
|
148
|
+
|
|
149
|
+
OpenCode `1.18.16+` requires an exact `provider/model` route and an explicit,
|
|
150
|
+
read-only configuration source. Configuration may define providers and an
|
|
151
|
+
exact default model, but it must not contain credential values. Name the
|
|
152
|
+
credential environment variables the caller is allowed to forward instead.
|
|
153
|
+
|
|
154
|
+
Callers that deliberately retain native OpenCode config and login can use
|
|
155
|
+
`OpenCode::Permissions.compile` directly and pass the JSON document through
|
|
156
|
+
OpenCode's per-process permission input. The prepared-overlay API below
|
|
157
|
+
remains the closed, isolated option.
|
|
158
|
+
|
|
159
|
+
```ruby
|
|
160
|
+
require "agent_cli_runtime"
|
|
161
|
+
require "tmpdir"
|
|
162
|
+
|
|
163
|
+
route = "anthropic/claude-sonnet-4-5"
|
|
164
|
+
profile = AgentCliRuntime::Profiles.fetch(:opencode)
|
|
165
|
+
request = AgentCliRuntime::Request.new(
|
|
166
|
+
profile: profile,
|
|
167
|
+
prompt: "Make the requested atomic edit",
|
|
168
|
+
permission_mode: "workspace-write",
|
|
169
|
+
model: route,
|
|
170
|
+
effort: "high"
|
|
171
|
+
)
|
|
172
|
+
preparation = AgentCliRuntime::OpenCodePreparationRequest.new(
|
|
173
|
+
request: request,
|
|
174
|
+
working_directory: Dir.pwd,
|
|
175
|
+
invocation_root: File.join(Dir.tmpdir, "my-opencode-invocation"),
|
|
176
|
+
configuration: {
|
|
177
|
+
"model" => route,
|
|
178
|
+
"provider" => {
|
|
179
|
+
"anthropic" => { "npm" => "@ai-sdk/anthropic" }
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
credential_environment_keys: ["ANTHROPIC_API_KEY"],
|
|
183
|
+
additional_read_roots: [Dir.pwd],
|
|
184
|
+
additional_write_roots: [Dir.pwd],
|
|
185
|
+
bash_patterns: ["git*", "bundle*", "bin/*"]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
prepared = AgentCliRuntime.prepare!(preparation)
|
|
189
|
+
begin
|
|
190
|
+
# The caller owns spawning, capture, timeout/cancellation, and process-tree
|
|
191
|
+
# cleanup. Forward only this selected environment to the child.
|
|
192
|
+
run_argv = prepared.invocation.argv
|
|
193
|
+
run_stdin = prepared.invocation.stdin_data
|
|
194
|
+
run_environment = prepared.environment_for(env: ENV)
|
|
195
|
+
|
|
196
|
+
# After a zero main-process exit, parse the captured JSONL and run the
|
|
197
|
+
# separately compiled, non-model sanitized export under the same overlay.
|
|
198
|
+
parsed = AgentCliRuntime.parse_run(:opencode, stdout: run_stdout)
|
|
199
|
+
inspection = AgentCliRuntime.prepare_inspection(prepared, parsed)
|
|
200
|
+
inspection_argv = inspection.argv
|
|
201
|
+
inspection_environment = inspection.environment_for(env: ENV)
|
|
202
|
+
|
|
203
|
+
captured = AgentCliRuntime::CapturedResult.new(
|
|
204
|
+
stdout: run_stdout,
|
|
205
|
+
stderr: run_stderr,
|
|
206
|
+
termination: AgentCliRuntime::TerminationEvidence.new(exit_code: 0),
|
|
207
|
+
inspection_output: sanitized_export_stdout
|
|
208
|
+
)
|
|
209
|
+
outcome = AgentCliRuntime.normalize(
|
|
210
|
+
:opencode, captured, requested_route: prepared.requested_route
|
|
211
|
+
)
|
|
212
|
+
ensure
|
|
213
|
+
prepared.cleanup! if prepared
|
|
214
|
+
end
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`run_stdout`, `run_stderr`, and `sanitized_export_stdout` above are captures
|
|
218
|
+
provided by the caller's process supervisor. Execute the inspection only after
|
|
219
|
+
a successful main run. For a timeout, cancellation, signal, or non-zero exit,
|
|
220
|
+
construct the matching `TerminationEvidence` and normalize without pretending
|
|
221
|
+
that incomplete output is a successful result.
|
|
222
|
+
|
|
223
|
+
Preparation creates owner-private config, data, cache, and state paths below
|
|
224
|
+
the fresh invocation root; redirects OpenCode into them; disables ambient
|
|
225
|
+
project/default discovery and remote model refresh; checks version, required
|
|
226
|
+
flags, selected auth, cached route, and requested variant locally; and returns
|
|
227
|
+
discrete argv/environment values. It never sends a prompt or model request.
|
|
228
|
+
`PreparedInvocation#cleanup!` removes only invocation-owned paths and is safe
|
|
229
|
+
to call twice. Call it from the process owner's `ensure` path after every
|
|
230
|
+
pre-spawn and post-spawn outcome.
|
|
231
|
+
|
|
232
|
+
`read-only` denies edits, shell, unsafe tools, and external writes.
|
|
233
|
+
`workspace-write` permits edits only under the declared write roots. Shell is
|
|
234
|
+
denied unless the caller supplies explicit OpenCode `bash_patterns`; those
|
|
235
|
+
patterns are application permissions, not an OS sandbox. A `nil` permission
|
|
236
|
+
mode is rejected unless the
|
|
237
|
+
consumer supplies an explicit typed `OpenCodePermissionPolicy`; the ordinary
|
|
238
|
+
preparation API never silently falls back to a bypass. Plugin sources are
|
|
239
|
+
explicit, and `--pure` remains enabled when no plugin was selected.
|
|
240
|
+
|
|
241
|
+
A completed outcome contains one bounded final assistant message, requested
|
|
242
|
+
and sanitized-export-observed routes, and nullable input/output/cache
|
|
243
|
+
read/cache write/reasoning/cost fields. Missing evidence stays `nil`; numeric
|
|
244
|
+
zero stays zero. Other outcome kinds are `authentication_failure`,
|
|
245
|
+
`configuration_failure`, `cli_failure`, `malformed_output`, `cancelled`, and
|
|
246
|
+
`timed_out`. Diagnostics and unknown-event summaries are bounded and redacted.
|
|
247
|
+
|
|
248
|
+
Maintainers can run the installed-CLI offline contract without a prompt or
|
|
249
|
+
model request:
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
bundle exec ruby -Itest test/opencode_offline_smoke_test.rb
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Auto-discovery considers every matching executable in `PATH` and prefers an
|
|
256
|
+
already-installed native executable over an earlier package-manager launcher,
|
|
257
|
+
including script and multicall-binary shims, so the
|
|
258
|
+
smoke cannot trigger package-manager installation or refresh behavior. If only
|
|
259
|
+
a shim is discoverable, set `AGENT_CLI_RUNTIME_OPENCODE_OFFLINE_BIN` to the
|
|
260
|
+
native OpenCode executable explicitly.
|
|
261
|
+
|
|
262
|
+
The authenticated atomic-edit smoke is separately gated and refuses to run
|
|
263
|
+
without an explicit route, config path, credential variable name, opt-in, and
|
|
264
|
+
non-empty selected credential:
|
|
265
|
+
|
|
266
|
+
```sh
|
|
267
|
+
AGENT_CLI_RUNTIME_OPENCODE_LIVE=1 \
|
|
268
|
+
AGENT_CLI_RUNTIME_OPENCODE_LIVE_ROUTE=anthropic/claude-sonnet-4-5 \
|
|
269
|
+
AGENT_CLI_RUNTIME_OPENCODE_LIVE_CONFIG=/absolute/path/opencode.json \
|
|
270
|
+
AGENT_CLI_RUNTIME_OPENCODE_LIVE_CREDENTIAL_ENV=ANTHROPIC_API_KEY \
|
|
271
|
+
ANTHROPIC_API_KEY=... \
|
|
272
|
+
bundle exec ruby -Itest test/opencode_live_test.rb
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The live test records only route, CLI version, outcome/usage availability, and
|
|
276
|
+
cleanup state. It does not print the credential or raw selected config. A
|
|
277
|
+
missing opt-in input is an explicit skip, not a deterministic-suite failure.
|
|
278
|
+
|
|
111
279
|
## Inspect local prerequisites
|
|
112
280
|
|
|
113
281
|
```ruby
|
|
@@ -126,7 +294,7 @@ agent-runtime probe --all --json
|
|
|
126
294
|
```
|
|
127
295
|
|
|
128
296
|
The JSON contract is `{"schema_version":1,"probes":[...]}` and always orders
|
|
129
|
-
all-provider output as `claude`, `codex`, `pi`, `grok`.
|
|
297
|
+
all-provider output as `claude`, `codex`, `pi`, `grok`, `opencode`.
|
|
130
298
|
|
|
131
299
|
- Exit `0`: every requested local probe is ready.
|
|
132
300
|
- Exit `1`: at least one requested local prerequisite is unavailable.
|
|
@@ -172,7 +340,7 @@ own provider-health policy.
|
|
|
172
340
|
## Compatibility
|
|
173
341
|
|
|
174
342
|
Provider flags, event formats, and public value-object fields are
|
|
175
|
-
SemVer-governed behavior. Additive fields are compatible within 0.
|
|
343
|
+
SemVer-governed behavior. Additive fields are compatible within 0.2.x; removing
|
|
176
344
|
or changing an existing field or meaning requires a new minor version while the
|
|
177
345
|
gem remains pre-1.0.
|
|
178
346
|
|
data/agent-cli-runtime.gemspec
CHANGED
|
@@ -5,12 +5,14 @@ Gem::Specification.new do |spec|
|
|
|
5
5
|
spec.version = AgentCliRuntime::VERSION
|
|
6
6
|
spec.authors = [ "Ivan Kuznetsov" ]
|
|
7
7
|
spec.email = [ "ivan@ikuznetsov.com" ]
|
|
8
|
-
spec.summary =
|
|
8
|
+
spec.summary =
|
|
9
|
+
"One Ruby API for Claude Code, Codex CLI, Pi, Grok CLI, and OpenCode"
|
|
9
10
|
spec.description = <<~DESC
|
|
10
|
-
Agent CLI Runtime
|
|
11
|
-
local
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
Agent CLI Runtime gives Ruby applications one stable integration layer for
|
|
12
|
+
local coding-agent CLIs. It builds provider-specific commands from a shared
|
|
13
|
+
request model, checks local versions and named capabilities, and normalizes
|
|
14
|
+
usage and results afterward. Centralized profiles, environment rules, and
|
|
15
|
+
parsers make agent providers easier to add, switch, and upgrade.
|
|
14
16
|
DESC
|
|
15
17
|
spec.homepage = "https://github.com/ivankuznetsov/agent-cli-runtime"
|
|
16
18
|
spec.license = "MIT"
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module AgentCliRuntime
|
|
2
|
+
# Provider-side failures that a CLI reports on its event stream while still
|
|
3
|
+
# exiting zero. Each profile owns the shape its own CLI emits. Extractors
|
|
4
|
+
# return either the raw provider text, an ExtractedFailure, or nil when the
|
|
5
|
+
# event carries no failure. Runtime.extract_provider_error normalizes and
|
|
6
|
+
# redacts the result.
|
|
7
|
+
module ErrorExtractors
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# CLIs that name the failure with a dedicated event type. This is the same
|
|
11
|
+
# set Hive scanned for inline before provider shape moved behind the
|
|
12
|
+
# profile, so wiring a profile to DEFAULT preserves its behaviour.
|
|
13
|
+
DEFAULT = lambda do |event|
|
|
14
|
+
next nil unless event.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
case event["type"]
|
|
17
|
+
when "error", "turn.failed", "rate_limit_event"
|
|
18
|
+
ErrorExtractors.message_from(event)
|
|
19
|
+
when "result"
|
|
20
|
+
if event["is_error"] == true || event["subtype"].to_s.start_with?("error")
|
|
21
|
+
ErrorExtractors.message_from(event)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Claude's subscription wall is a dedicated structured event with no
|
|
27
|
+
# human-readable message. The generic extractor therefore returned nil,
|
|
28
|
+
# leaving callers to flatten a rejected five-hour/seven-day window into a
|
|
29
|
+
# generic exit-code failure. Accept only the exact rejected account-window
|
|
30
|
+
# shapes observed from Claude Code and synthesize bounded provider text for
|
|
31
|
+
# the normal typed limit path. Other Claude errors retain DEFAULT behavior.
|
|
32
|
+
CLAUDE_ACCOUNT_LIMIT_TYPES = %w[five_hour seven_day].freeze
|
|
33
|
+
CLAUDE = lambda do |event|
|
|
34
|
+
if event.is_a?(Hash) && event["type"] == "rate_limit_event"
|
|
35
|
+
info = event["rate_limit_info"]
|
|
36
|
+
if info.is_a?(Hash) && info["status"] == "rejected"
|
|
37
|
+
limit_type = info["rateLimitType"].to_s
|
|
38
|
+
if CLAUDE_ACCOUNT_LIMIT_TYPES.include?(limit_type)
|
|
39
|
+
next ExtractedFailure.new(
|
|
40
|
+
kind: :provider_limit,
|
|
41
|
+
message: "Claude account quota reached (#{limit_type})"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
DEFAULT.call(event)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# pi keeps the envelope type ("message_start"/"message_end") and moves the
|
|
51
|
+
# terminal state into stopReason. Provider refusals use
|
|
52
|
+
# stopReason=error/errorMessage. A model that consumes its entire output
|
|
53
|
+
# allowance uses stopReason=length and can exit zero without executing the
|
|
54
|
+
# final write tool. Matching on type alone sees neither failure.
|
|
55
|
+
PI = lambda do |event|
|
|
56
|
+
next nil unless event.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
message = event["message"].is_a?(Hash) ? event["message"] : event
|
|
59
|
+
case message["stopReason"].to_s
|
|
60
|
+
when "error"
|
|
61
|
+
ErrorExtractors.message_from(message) || ErrorExtractors.message_from(event)
|
|
62
|
+
when "length"
|
|
63
|
+
ExtractedFailure.new(
|
|
64
|
+
kind: :model_output_limit,
|
|
65
|
+
message: "model response reached its maximum output tokens"
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# OpenCode keeps provider failures under error.data.message rather than
|
|
71
|
+
# the top-level/message and error.message shapes used by the default
|
|
72
|
+
# extractor. Read only the dedicated error event so ordinary model text
|
|
73
|
+
# cannot forge a provider-limit signal.
|
|
74
|
+
OPENCODE = lambda do |event|
|
|
75
|
+
next nil unless event.is_a?(Hash) && event["type"] == "error"
|
|
76
|
+
|
|
77
|
+
data = event.dig("error", "data")
|
|
78
|
+
message = data["message"] if data.is_a?(Hash)
|
|
79
|
+
message if message.is_a?(String) && !message.strip.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def message_from(event)
|
|
83
|
+
return nil unless event.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
candidates = [
|
|
86
|
+
event["errorMessage"],
|
|
87
|
+
event["message"],
|
|
88
|
+
event.dig("error", "message"),
|
|
89
|
+
event["error"]
|
|
90
|
+
]
|
|
91
|
+
text = candidates.find { |value| value.is_a?(String) && !value.strip.empty? }
|
|
92
|
+
text&.strip
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -14,4 +14,11 @@ module AgentCliRuntime
|
|
|
14
14
|
class ProbeError < Error; end
|
|
15
15
|
class CompilationError < Error; end
|
|
16
16
|
class UnknownProvider < Error; end
|
|
17
|
+
class PreparationError < Error; end
|
|
18
|
+
class ConfigurationError < PreparationError; end
|
|
19
|
+
class AuthenticationError < ProbeError; end
|
|
20
|
+
class RouteUnavailable < ProbeError; end
|
|
21
|
+
class UnsafePathError < PreparationError; end
|
|
22
|
+
class ResultError < Error; end
|
|
23
|
+
class MalformedOutput < ResultError; end
|
|
17
24
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module AgentCliRuntime
|
|
2
|
+
module OpenCode
|
|
3
|
+
module Inspection
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def compile(prepared, parsed_run)
|
|
7
|
+
unless prepared.is_a?(PreparedInvocation)
|
|
8
|
+
raise ArgumentError,
|
|
9
|
+
"prepared must be an AgentCliRuntime::PreparedInvocation"
|
|
10
|
+
end
|
|
11
|
+
unless parsed_run.is_a?(ParsedRun)
|
|
12
|
+
raise ArgumentError, "parsed_run must be an AgentCliRuntime::ParsedRun"
|
|
13
|
+
end
|
|
14
|
+
unless prepared.invocation.provider == :opencode
|
|
15
|
+
raise ConfigurationError,
|
|
16
|
+
"sanitized export inspection requires an OpenCode invocation"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
InspectionCommand.new(
|
|
20
|
+
argv: [
|
|
21
|
+
prepared.executable, "export", parsed_run.session_id, "--sanitize"
|
|
22
|
+
],
|
|
23
|
+
stdin_data: nil,
|
|
24
|
+
environment: prepared.environment,
|
|
25
|
+
credential_environment_keys: prepared.credential_environment_keys,
|
|
26
|
+
session_id: parsed_run.session_id,
|
|
27
|
+
message_id: parsed_run.terminal_message_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|