agent-harness 0.38.0 → 0.39.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/README.md +6 -0
- data/docs/provider-neutral-api-execution-contract.md +532 -0
- data/lib/agent_harness/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6b6b9b84a3074d82a44dd35a6b377628c64cfa01958ca5c41ec910b694375fcf
|
|
4
|
+
data.tar.gz: 8069427d9a79624deba026984490a5f25cf97df7364c6d78b44c2a43aef773c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cad34514f544dcf13a470afbb7cbdde82269246ee16d97319e3e6f33afd2e663b0625fd2f088bc4c68ef491676ac3c5cfc80e6bc3ba5a030fbda1abbe9a04cf7
|
|
7
|
+
data.tar.gz: 48fd85599fca1ae94fe13e433d4a69d1ab5bd8ce4c9c8feab6388c79c2d48c5314467cba8c4fcb277662c1aea20f53206b35fef6cad0a82ef82618249a80a883
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* add runner model compatibility contract (`AgentHarness.model_compatibility`) with structured `ModelCompatibility::Result` outcomes. Codex exposes static facts for CLI-gated models (e.g. `gpt-5.5` requires Codex CLI `>= 0.116.0`), a baseline supported-model list, supported auth modes, and a `DEFAULT_COMPATIBLE_MODEL_ID` fallback so downstream orchestrators can validate tier/model assignments before scheduling agent runs ([#259](https://github.com/viamin/agent-harness/issues/259)).
|
|
6
6
|
* **auth:** add provider-owned PKCE code-exchange API for Claude OAuth (`AgentHarness::Authentication.exchange_code`). Takes an authorization code plus PKCE verifier (and `redirect_uri`/`client_id`), posts an `authorization_code` grant to the Claude token endpoint, and persists the resulting access/refresh tokens in the native `claudeAiOauth` shape. Adds `exchange_code_supported?` and a `code_exchange` key to `auth_capabilities` ([#266](https://github.com/viamin/agent-harness/issues/266)).
|
|
7
7
|
|
|
8
|
+
## [0.39.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.38.0...agent-harness/v0.39.0) (2026-09-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* design: Define the Provider-Neutral API Execution Contract (RDR-072) ([#438](https://github.com/viamin/agent-harness/issues/438)) ([ae2fc6b](https://github.com/viamin/agent-harness/commit/ae2fc6bafad54ac35bb9d30a32d253c68a7396f2))
|
|
14
|
+
|
|
8
15
|
## [0.38.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.37.5...agent-harness/v0.38.0) (2026-09-24)
|
|
9
16
|
|
|
10
17
|
|
data/README.md
CHANGED
|
@@ -738,6 +738,12 @@ Health checks run five steps per provider: registration, CLI availability, authe
|
|
|
738
738
|
|
|
739
739
|
## Development
|
|
740
740
|
|
|
741
|
+
The proposed provider-neutral API execution boundary for embeddings, chat,
|
|
742
|
+
structured output, usage, and optional persistence is documented in the
|
|
743
|
+
[Provider-Neutral API Execution Contract](docs/provider-neutral-api-execution-contract.md).
|
|
744
|
+
It is a docs-only design boundary; capability support requires the release
|
|
745
|
+
evidence described there.
|
|
746
|
+
|
|
741
747
|
```bash
|
|
742
748
|
# Install dependencies
|
|
743
749
|
bin/setup
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# Provider-Neutral API Execution Contract
|
|
2
|
+
|
|
3
|
+
This document records the design investigation for
|
|
4
|
+
[RDR-072](https://github.com/viamin/paid/blob/01672a9992853e89bcbd272b720ac9f1da73cba0/docs/rdrs/RDR-072-api-conversation-delegation.md).
|
|
5
|
+
It is the upstream contract for incremental embedding, chat, structured-output,
|
|
6
|
+
usage, and optional conversation-persistence work.
|
|
7
|
+
|
|
8
|
+
## Status and rollout boundary
|
|
9
|
+
|
|
10
|
+
This is a design contract, not a claim that the described API is implemented.
|
|
11
|
+
RDR-072's rollout guard is **docs-only now**, so this change adds no runtime
|
|
12
|
+
behavior or dependency. Each capability below needs its own failing-first
|
|
13
|
+
contract tests, implementation, release evidence, and downstream adoption
|
|
14
|
+
evidence before a caller enables it.
|
|
15
|
+
|
|
16
|
+
Existing CLI and subscription behavior remains the default. Existing
|
|
17
|
+
`TextTransport`, `OpenAICompatibleTransport`, `Conversation`, and `Response`
|
|
18
|
+
interfaces remain available, but they do not yet satisfy this contract. A
|
|
19
|
+
caller must not infer support from a gem version or issue closure.
|
|
20
|
+
|
|
21
|
+
Normative words such as MUST and MUST NOT describe the future public boundary.
|
|
22
|
+
|
|
23
|
+
## Ownership boundary
|
|
24
|
+
|
|
25
|
+
AgentHarness owns protocol translation and one bounded provider request:
|
|
26
|
+
|
|
27
|
+
- capability discovery and explicit unsupported outcomes;
|
|
28
|
+
- provider-specific request and response translation;
|
|
29
|
+
- request retries within caller-supplied limits;
|
|
30
|
+
- normalized stream events, results, errors, and attempt reports; and
|
|
31
|
+
- stable attempt and tool-call identities at its public boundary.
|
|
32
|
+
|
|
33
|
+
The caller owns application authority and durable policy:
|
|
34
|
+
|
|
35
|
+
- tenant and actor identity, authorization, and visible tools;
|
|
36
|
+
- candidate order, credentials, allowed models, and fallback notices;
|
|
37
|
+
- budgets, cancellation initiation, runner changes, and workflow recovery;
|
|
38
|
+
- durable usage attribution and idempotent ingestion of attempt reports;
|
|
39
|
+
- tool confirmation and side-effect reconciliation; and
|
|
40
|
+
- application conversation/message identity and audit records.
|
|
41
|
+
|
|
42
|
+
The harness MUST NOT select an unlisted fallback, borrow global credentials,
|
|
43
|
+
change authentication mode, execute a tool, or replay a completed tool result.
|
|
44
|
+
Keeping the caller's chat loop over this normalized transport is an acceptable
|
|
45
|
+
final architecture.
|
|
46
|
+
|
|
47
|
+
## Common execution request
|
|
48
|
+
|
|
49
|
+
All operations use an immutable, request-local configuration. The eventual
|
|
50
|
+
Ruby API MAY use value objects rather than hashes, but it MUST expose these
|
|
51
|
+
semantics:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
request = {
|
|
55
|
+
request_id: "paid-generation-018f...", # caller-generated, stable on redelivery
|
|
56
|
+
operation: :chat, # :chat, :embedding, or :schema
|
|
57
|
+
candidates: [ # ordered; first entry is initial
|
|
58
|
+
{
|
|
59
|
+
provider: :anthropic,
|
|
60
|
+
model: "claude-sonnet-4-5",
|
|
61
|
+
protocol: :messages, # optional verified protocol
|
|
62
|
+
authentication_mode: :api_key,
|
|
63
|
+
endpoint: "https://llm-proxy.example/v1",
|
|
64
|
+
headers: {"X-Tenant-Route" => "tenant-123"},
|
|
65
|
+
credentials: {api_key: anthropic_secret}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
provider: :openai,
|
|
69
|
+
model: "gpt-5",
|
|
70
|
+
protocol: :responses,
|
|
71
|
+
authentication_mode: :api_key,
|
|
72
|
+
endpoint: "https://api.openai.com/v1",
|
|
73
|
+
headers: {},
|
|
74
|
+
credentials: {api_key: openai_secret}
|
|
75
|
+
}
|
|
76
|
+
],
|
|
77
|
+
fallback: {on_error_categories: [:transient]},
|
|
78
|
+
timeout: {connect_seconds: 5, read_seconds: 60},
|
|
79
|
+
retry: {max_attempts: 3, base_delay_seconds: 0.25, max_delay_seconds: 2},
|
|
80
|
+
cancellation: cancellation_token,
|
|
81
|
+
metadata: {tenant_id: "tenant-123", workflow_id: "workflow-456"}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`candidates` MUST contain at least one complete candidate record. Its first
|
|
86
|
+
entry is the initial candidate; later entries are the only authorized fallback
|
|
87
|
+
order. A request that does not authorize fallback supplies a one-entry array.
|
|
88
|
+
`fallback.on_error_categories` is the caller-selected allowlist of error
|
|
89
|
+
categories that permit advancing to the next entry; an absent or empty
|
|
90
|
+
allowlist disables fallback. Candidate records, their order, and the allowlist
|
|
91
|
+
are immutable for the lifetime of the request. The observer described below is
|
|
92
|
+
a request-local callback or equivalent API argument because callbacks are not
|
|
93
|
+
part of a serializable request document.
|
|
94
|
+
|
|
95
|
+
`request_id` identifies one logical operation. Every physical outbound request
|
|
96
|
+
gets a distinct `attempt_id`. Redelivering an already reported attempt retains
|
|
97
|
+
its `attempt_id`; initiating another outbound request does not.
|
|
98
|
+
|
|
99
|
+
Credentials, endpoint, headers, timeouts, retry limits, and cancellation are
|
|
100
|
+
request-local. Implementations MUST prevent concurrent requests from observing
|
|
101
|
+
one another's credentials or headers. They MUST reject reserved header
|
|
102
|
+
overrides that would conflict with the selected protocol's authentication.
|
|
103
|
+
Logs and errors MUST NOT contain credentials, authorization headers, message
|
|
104
|
+
bodies, tool arguments, or full provider responses.
|
|
105
|
+
|
|
106
|
+
The selected candidate's provider, model, protocol, endpoint, and authentication
|
|
107
|
+
mode MUST be the values actually used. The harness MUST return a configuration
|
|
108
|
+
or unsupported outcome instead of silently substituting any of them.
|
|
109
|
+
|
|
110
|
+
## Capability discovery and unsupported outcomes
|
|
111
|
+
|
|
112
|
+
Support is declared per operation, provider, protocol, model when known, and
|
|
113
|
+
authentication mode. It is not a single provider-wide Boolean. The discovery
|
|
114
|
+
result has one of three states:
|
|
115
|
+
|
|
116
|
+
- `supported`: the requested combination is verified;
|
|
117
|
+
- `unsupported`: it is known not to work, with a stable reason; or
|
|
118
|
+
- `unknown`: it has not been verified and MUST NOT be routed as supported.
|
|
119
|
+
|
|
120
|
+
Stable unsupported reasons initially include:
|
|
121
|
+
|
|
122
|
+
- `operation_not_supported`;
|
|
123
|
+
- `protocol_not_supported`;
|
|
124
|
+
- `authentication_mode_not_supported`;
|
|
125
|
+
- `custom_endpoint_not_supported`;
|
|
126
|
+
- `custom_headers_not_supported`;
|
|
127
|
+
- `streaming_not_supported`;
|
|
128
|
+
- `tools_not_supported`;
|
|
129
|
+
- `structured_output_not_supported`; and
|
|
130
|
+
- `state_restoration_not_supported`.
|
|
131
|
+
|
|
132
|
+
An unsupported result is a normal, non-retryable outcome carrying the requested
|
|
133
|
+
scope and reason. It is not a generic provider failure. Callers MAY choose
|
|
134
|
+
another candidate only from their own ordered list.
|
|
135
|
+
|
|
136
|
+
## Normalized chat contract
|
|
137
|
+
|
|
138
|
+
### Messages
|
|
139
|
+
|
|
140
|
+
The input is an ordered array. Each message has a stable caller-owned `id`, one
|
|
141
|
+
of `system`, `user`, `assistant`, or `tool` as its `role`, and an ordered
|
|
142
|
+
`content` array. Content parts initially support `text`; unimplemented media
|
|
143
|
+
parts produce an explicit unsupported outcome. Assistant messages MAY contain
|
|
144
|
+
tool calls. Tool messages MUST reference exactly one `tool_call_id`.
|
|
145
|
+
|
|
146
|
+
Provider-specific wire roles, system-message placement, and content block
|
|
147
|
+
shapes stay behind the harness boundary. The harness MUST preserve ordering,
|
|
148
|
+
empty assistant text accompanying tool calls, and unknown versus absent data.
|
|
149
|
+
|
|
150
|
+
### Tools
|
|
151
|
+
|
|
152
|
+
Tool definitions contain a caller-owned name, description, and JSON Schema
|
|
153
|
+
input definition. A normalized tool call contains:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
{
|
|
157
|
+
id: "toolcall_018f...", # stable harness ID
|
|
158
|
+
provider_id: "call_abc", # optional provider correlation only
|
|
159
|
+
name: "search_documents",
|
|
160
|
+
arguments_json: "{\"query\":\"Ruby\"}"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The stable `id` is generated before the call is exposed or persisted and MUST
|
|
165
|
+
survive export/import. `provider_id` is not a durable application identifier.
|
|
166
|
+
Arguments remain JSON text until parsing succeeds; malformed arguments produce
|
|
167
|
+
a classified parse failure and are never executed by the harness. Tool results
|
|
168
|
+
reference the stable ID. Redelivery or restoration MUST skip a tool call when a
|
|
169
|
+
completed result for that ID exists.
|
|
170
|
+
|
|
171
|
+
The transport never decides approval and never executes application tools.
|
|
172
|
+
|
|
173
|
+
### Streaming
|
|
174
|
+
|
|
175
|
+
The stream is ordered and contains these normalized event types:
|
|
176
|
+
|
|
177
|
+
- `response_started` with request and attempt identity;
|
|
178
|
+
- `text_delta` with appended text;
|
|
179
|
+
- `tool_call_started`, `tool_call_delta`, and `tool_call_completed`;
|
|
180
|
+
- `usage_updated` when defensible cumulative or delta usage is available;
|
|
181
|
+
- `response_completed` with the final normalized result; and
|
|
182
|
+
- `response_failed` or `response_cancelled` with the partial result.
|
|
183
|
+
|
|
184
|
+
Every event carries `request_id`, `attempt_id`, and a monotonically increasing
|
|
185
|
+
`sequence`. A terminal event occurs exactly once per attempt. Callback failure
|
|
186
|
+
requests cancellation and surfaces as a caller error; it MUST NOT be converted
|
|
187
|
+
to provider success.
|
|
188
|
+
|
|
189
|
+
If a stream ends after emitting content, the terminal result has
|
|
190
|
+
`status: :partial`, retains received text and completed tool calls, and carries
|
|
191
|
+
the classified error. Incomplete tool calls are marked incomplete and MUST NOT
|
|
192
|
+
be executed. A partial attempt is never transparently retried: already emitted
|
|
193
|
+
content cannot be withdrawn. The caller must explicitly decide whether a new
|
|
194
|
+
attempt is safe and how to represent the abandoned partial message.
|
|
195
|
+
|
|
196
|
+
### Result
|
|
197
|
+
|
|
198
|
+
A completed chat or schema result contains:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
{
|
|
202
|
+
request_id: "paid-generation-018f...",
|
|
203
|
+
status: :succeeded, # :succeeded, :partial, :failed, or :cancelled
|
|
204
|
+
provider: :anthropic,
|
|
205
|
+
model: "claude-sonnet-4-5",
|
|
206
|
+
authentication_mode: :api_key,
|
|
207
|
+
content: "...",
|
|
208
|
+
parsed: nil,
|
|
209
|
+
tool_calls: [],
|
|
210
|
+
finish_reason: :stop,
|
|
211
|
+
attempts: [],
|
|
212
|
+
usage: {input_tokens: 12, output_tokens: 8, total_tokens: 20},
|
|
213
|
+
provider_request_id: nil,
|
|
214
|
+
error: nil
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`usage` aggregates only the attached attempt reports. Missing provider values
|
|
219
|
+
remain `nil`; they are not coerced to zero. `provider_request_id` is optional
|
|
220
|
+
diagnostic correlation and is not the accounting identity.
|
|
221
|
+
|
|
222
|
+
## Structured-output contract
|
|
223
|
+
|
|
224
|
+
A schema operation adds a JSON Schema and optional schema name to the chat
|
|
225
|
+
request. The capability check MUST cover the selected provider/model/protocol
|
|
226
|
+
and requested schema mode. JSON-only mode is not equivalent to schema-enforced
|
|
227
|
+
output and MUST be reported separately.
|
|
228
|
+
|
|
229
|
+
On success, `content` preserves provider JSON text and `parsed` contains the
|
|
230
|
+
parsed Ruby value. Invalid JSON or schema mismatch is a classified,
|
|
231
|
+
non-transient result with the original text retained. The harness MUST NOT
|
|
232
|
+
silently strip fences, repair content, issue a second model call, or switch to
|
|
233
|
+
a different credential to make parsing succeed.
|
|
234
|
+
|
|
235
|
+
## Embedding contract
|
|
236
|
+
|
|
237
|
+
An embedding request supplies one string or an ordered array of strings,
|
|
238
|
+
provider/model candidate, and optional dimensions. Its result preserves input
|
|
239
|
+
order and contains dense vectors, the model actually used, per-attempt usage,
|
|
240
|
+
and optional provider request correlation. One input returns one vector;
|
|
241
|
+
multiple inputs return an equal-length vector array.
|
|
242
|
+
|
|
243
|
+
Empty batches, unsupported dimensions, unsupported input media, and batch-size
|
|
244
|
+
limits are explicit configuration or unsupported outcomes. The first release
|
|
245
|
+
need not cover multimodal or provider-side batch APIs. Vector persistence and
|
|
246
|
+
similarity search remain caller responsibilities.
|
|
247
|
+
|
|
248
|
+
## Attempts, usage, retries, and cancellation
|
|
249
|
+
|
|
250
|
+
Each physical provider request produces an attempt report, including failed and
|
|
251
|
+
cancelled attempts:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
{
|
|
255
|
+
attempt_id: "attempt_018f...",
|
|
256
|
+
request_id: "paid-generation-018f...",
|
|
257
|
+
number: 2,
|
|
258
|
+
provider: :anthropic,
|
|
259
|
+
model: "claude-sonnet-4-5",
|
|
260
|
+
status: :failed,
|
|
261
|
+
started_at: "2026-09-25T12:00:00Z",
|
|
262
|
+
finished_at: "2026-09-25T12:00:01Z",
|
|
263
|
+
usage: {input_tokens: nil, output_tokens: nil, total_tokens: nil},
|
|
264
|
+
cost: nil,
|
|
265
|
+
provider_reported: false,
|
|
266
|
+
error: {category: :transient, code: :service_unavailable}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Attempt reports are delivered both with the final result and through an
|
|
271
|
+
observer so durable accounting can persist an attempt even when no message is
|
|
272
|
+
created. Callers deduplicate on `attempt_id`. Cost identifies its source as
|
|
273
|
+
provider-reported or harness-estimated; unknown cost remains `nil`.
|
|
274
|
+
|
|
275
|
+
Only errors classified `transient` are eligible for bounded request retry:
|
|
276
|
+
connection failure, timeout before a partial stream, rate limit, server error,
|
|
277
|
+
service unavailable, and overload. Authentication, authorization, billing,
|
|
278
|
+
invalid request, unsupported capability, invalid schema, context length,
|
|
279
|
+
configuration, and cancellation are non-retryable.
|
|
280
|
+
|
|
281
|
+
The supplied `max_attempts` includes the first outbound request and is the total
|
|
282
|
+
physical-attempt budget across all candidates; changing candidates does not
|
|
283
|
+
reset it. It MUST be a positive integer; the harness rejects zero, negative, or
|
|
284
|
+
non-integer values as configuration errors before making an outbound request.
|
|
285
|
+
Delay and provider `retry-after` handling remain within the supplied bounds.
|
|
286
|
+
Cancellation is checked before an attempt, during backoff, while reading a
|
|
287
|
+
stream, and before returning success. It stops further attempts and returns a
|
|
288
|
+
cancelled terminal outcome with any partial usage. Cancellation does not prove
|
|
289
|
+
that the provider stopped processing or billing the request.
|
|
290
|
+
|
|
291
|
+
The harness owns the complete attempt sequence; there is exactly one retry
|
|
292
|
+
owner. RubyLLM's Faraday retry middleware resends the current candidate inside
|
|
293
|
+
the transport before a classified error can return to the harness, so
|
|
294
|
+
middleware-driven retries cannot honor the fallback-first order below and would
|
|
295
|
+
spend the shared budget on one candidate invisibly to the observer. An adapter
|
|
296
|
+
backed by RubyLLM MUST therefore disable its retry middleware with
|
|
297
|
+
`max_retries: 0`, overriding the default three retries, so that one adapter
|
|
298
|
+
call performs exactly one physical outbound request. The harness then issues
|
|
299
|
+
one adapter call per attempt — initial, retry, or fallback — and applies the
|
|
300
|
+
shared `max_attempts` budget, cancellation checks, backoff, and provider
|
|
301
|
+
`retry-after` between calls. No component may retry beneath the harness, and
|
|
302
|
+
the harness MUST NOT delegate this sequencing to middleware or another nested
|
|
303
|
+
loop. Existing conductor retry/provider switching is not part of an API
|
|
304
|
+
request's internal retry budget and MUST be disabled or bypassed for a
|
|
305
|
+
migrated scope.
|
|
306
|
+
|
|
307
|
+
## Caller-controlled fallback
|
|
308
|
+
|
|
309
|
+
Fallback candidates are the entries after the first entry in the request's
|
|
310
|
+
ordered `candidates` array. Each is a complete request-local candidate record,
|
|
311
|
+
including credentials and endpoint/header overrides. A candidate change is a
|
|
312
|
+
new caller-authorized selection, not a hidden retry. Before advancing, the
|
|
313
|
+
harness invokes the request-local observer with the current and next candidate
|
|
314
|
+
identities and the classified error, so the caller can cancel the change or
|
|
315
|
+
issue a notice. The harness reports every candidate attempt.
|
|
316
|
+
|
|
317
|
+
Fallback is allowed only for caller-selected error categories. It never occurs
|
|
318
|
+
after a partial stream without a new explicit caller decision, never crosses
|
|
319
|
+
authentication modes implicitly, and never replays completed tools. The
|
|
320
|
+
harness MUST NOT use RubyLLM global fallback or global credential configuration
|
|
321
|
+
where it could exceed this list or leak request-local configuration.
|
|
322
|
+
|
|
323
|
+
Fallback takes precedence over retry when both are eligible. After a failed
|
|
324
|
+
attempt, the harness MUST apply this order:
|
|
325
|
+
|
|
326
|
+
1. Stop if cancellation was requested or any partial stream was exposed.
|
|
327
|
+
2. If the budget has capacity, the error category is in
|
|
328
|
+
`fallback.on_error_categories`, and another candidate remains, notify the
|
|
329
|
+
observer and advance to that candidate.
|
|
330
|
+
3. Otherwise, retry the current candidate only when the error is retryable and
|
|
331
|
+
the shared `max_attempts` budget has capacity.
|
|
332
|
+
4. Otherwise, return the terminal error.
|
|
333
|
+
|
|
334
|
+
The outbound request after either advancing or retrying consumes one physical
|
|
335
|
+
attempt from the same budget. Candidates are never revisited, and a failed
|
|
336
|
+
observer notification or observer cancellation stops before that request.
|
|
337
|
+
Consequently, the example request attempts Anthropic once and then OpenAI after
|
|
338
|
+
an eligible `transient` failure; only OpenAI can consume the remaining
|
|
339
|
+
same-candidate retry budget.
|
|
340
|
+
|
|
341
|
+
## Error contract
|
|
342
|
+
|
|
343
|
+
Every terminal error exposes a stable category and code, retry eligibility,
|
|
344
|
+
provider/model identity, optional sanitized status and request ID, and partial
|
|
345
|
+
result/usage where available. Categories are:
|
|
346
|
+
|
|
347
|
+
| Category | Examples | Request retry |
|
|
348
|
+
| --- | --- | --- |
|
|
349
|
+
| `transient` | timeout, connection failure, 429, 5xx, overload | bounded |
|
|
350
|
+
| `authentication` | missing or rejected credential | never |
|
|
351
|
+
| `authorization` | credential lacks access | never |
|
|
352
|
+
| `billing` | inactive billing account or payment required | never |
|
|
353
|
+
| `configuration` | invalid endpoint/header/model or local configuration | never |
|
|
354
|
+
| `invalid_request` | malformed or semantically invalid provider request | never |
|
|
355
|
+
| `context_length` | request exceeds the model context window | never |
|
|
356
|
+
| `unsupported` | operation or option not implemented | never |
|
|
357
|
+
| `invalid_response` | malformed JSON, schema/tool parse failure | never |
|
|
358
|
+
| `cancelled` | caller cancellation | never |
|
|
359
|
+
| `caller` | callback or local input failure | never |
|
|
360
|
+
| `unknown` | provider failure that cannot be classified confidently | never |
|
|
361
|
+
|
|
362
|
+
Adapters MUST use the following category and code mappings. A provider-specific
|
|
363
|
+
status or error name may be retained as sanitized metadata, but MUST NOT replace
|
|
364
|
+
these values or change fallback eligibility.
|
|
365
|
+
|
|
366
|
+
| Failure | Category | Code |
|
|
367
|
+
| --- | --- | --- |
|
|
368
|
+
| Connection failure | `transient` | `connection_failed` |
|
|
369
|
+
| Timeout before a partial stream | `transient` | `timeout` |
|
|
370
|
+
| Rate limit | `transient` | `rate_limited` |
|
|
371
|
+
| Provider server error | `transient` | `server_error` |
|
|
372
|
+
| Service unavailable | `transient` | `service_unavailable` |
|
|
373
|
+
| Provider overload | `transient` | `overloaded` |
|
|
374
|
+
| Missing or rejected credential | `authentication` | `invalid_credential` |
|
|
375
|
+
| Credential lacks access | `authorization` | `permission_denied` |
|
|
376
|
+
| Billing account inactive or payment required | `billing` | `billing_unavailable` |
|
|
377
|
+
| Invalid provider request | `invalid_request` | `invalid_request` |
|
|
378
|
+
| Unsupported capability or option | `unsupported` | `unsupported_capability` |
|
|
379
|
+
| Structured output violates its schema | `invalid_response` | `invalid_schema` |
|
|
380
|
+
| Request exceeds the model context window | `context_length` | `context_length_exceeded` |
|
|
381
|
+
| Invalid endpoint, header, model, or local request configuration | `configuration` | `invalid_configuration` |
|
|
382
|
+
| Caller cancellation | `cancelled` | `cancelled` |
|
|
383
|
+
|
|
384
|
+
If a failure cannot be mapped confidently, the adapter MUST return `unknown` /
|
|
385
|
+
`unclassified_provider_error`, which is non-retryable, rather than guessing a
|
|
386
|
+
retryable category. A timeout or other error after a partial stream retains its
|
|
387
|
+
mapped category and code, but its per-error retry eligibility is `false` and the
|
|
388
|
+
fallback rule above prohibits automatic replay.
|
|
389
|
+
|
|
390
|
+
Provider error classes stay internal. Existing `ProviderError` is too broad to
|
|
391
|
+
drive this policy and existing `ErrorTaxonomy` was designed for CLI provider
|
|
392
|
+
switching, so neither is the future API retry contract without an explicit
|
|
393
|
+
mapping and contract tests.
|
|
394
|
+
|
|
395
|
+
## State export, import, and restart safety
|
|
396
|
+
|
|
397
|
+
Plain Ruby state export is a versioned data document, not a serialized Ruby
|
|
398
|
+
object. It contains normalized messages, stable message/tool-call IDs, completed
|
|
399
|
+
tool results, pending tool calls and decisions, request IDs, and attempt reports.
|
|
400
|
+
It never contains credentials, callbacks, open streams, tool implementations,
|
|
401
|
+
authorization decisions, or mutable provider client objects.
|
|
402
|
+
|
|
403
|
+
Import validates the version and structure and reconstructs the normalized
|
|
404
|
+
conversation. The caller must re-supply tools, authorization, candidates,
|
|
405
|
+
credentials, observers, retry limits, and cancellation. Unknown versions fail
|
|
406
|
+
explicitly. Export/import round-trip tests must prove that completed tool IDs
|
|
407
|
+
are not re-executed and unknown usage remains unknown.
|
|
408
|
+
|
|
409
|
+
The restart guarantee is limited to checkpoints. A saved provider response or
|
|
410
|
+
tool result is skipped after restoration. A process crash after an external
|
|
411
|
+
tool side effect but before its result is durably saved can execute the tool
|
|
412
|
+
again. No transcript format can close that window. Tools therefore need a
|
|
413
|
+
caller-owned idempotency key derived from the stable tool-call ID, or explicit
|
|
414
|
+
reconciliation before replay. Provider requests have the same ambiguity when a
|
|
415
|
+
response is lost after the provider accepted it.
|
|
416
|
+
|
|
417
|
+
## RubyLLM 2.0 mapping and alternatives
|
|
418
|
+
|
|
419
|
+
The investigation used RubyLLM 2.0.0. RubyLLM remains an implementation detail;
|
|
420
|
+
the harness public API MUST NOT expose its classes or private persistence
|
|
421
|
+
records.
|
|
422
|
+
|
|
423
|
+
| Contract area | RubyLLM 2.0 mapping | Decision or gap |
|
|
424
|
+
| --- | --- | --- |
|
|
425
|
+
| Chat/protocols | `RubyLLM.chat`, messages, tools, stream callbacks | Candidate adapter; normalize all values and errors |
|
|
426
|
+
| Schema | `with_schema`, `response.parsed`, model capability registry | Candidate; distinguish enforced schema from JSON mode |
|
|
427
|
+
| Embeddings | `RubyLLM.embed` and normalized vectors/usage | Candidate first capability; persistence remains in Paid |
|
|
428
|
+
| Custom headers | `with_headers` | Candidate; contract-test merging and secret redaction |
|
|
429
|
+
| Endpoint/credentials | provider configuration | Global mutable configuration is unsuitable; require request-local isolation or an upstream-supported client boundary |
|
|
430
|
+
| Retries | Faraday retry middleware, default three retries | Disable with `max_retries: 0` so one call is one physical attempt; the harness sequences bounded retry and fallback itself; never nest |
|
|
431
|
+
| Fallback | `with_fallbacks` and callbacks | Harness sequences candidates per call; `with_fallbacks` usable only if exact candidates, credentials, and per-advance observer control are preserved |
|
|
432
|
+
| Cancellation | chat cancellation and `CancelledError` | Adapt to the common token and retain partial stream state |
|
|
433
|
+
| Attempt usage | `usage.ruby_llm` per physical attempt | Useful facts, but the public payload has no stable attempt ID; harness must add one |
|
|
434
|
+
| Plain Ruby resume | transcript can be reconstructed manually | No documented state export/import API; implement normalized export/import outside RubyLLM |
|
|
435
|
+
| Rails resume | `acts_as_chat` transcript plus supporting records | Technically restart-safe at checkpoints; at-least-once side effects remain |
|
|
436
|
+
|
|
437
|
+
### Optional Rails supporting tables
|
|
438
|
+
|
|
439
|
+
RubyLLM 2.0 owns `ruby_llm_models`, `ruby_llm_tool_calls`,
|
|
440
|
+
`ruby_llm_usages`, and `ruby_llm_batches`. Only the middle two are candidates
|
|
441
|
+
for early chat adoption; batches are out of scope, while the model registry is
|
|
442
|
+
needed only if capability/pricing lookup uses it.
|
|
443
|
+
|
|
444
|
+
`ruby_llm_tool_calls.tool_call_id` is unique and can preserve the provider tool
|
|
445
|
+
request/result link. It is acceptable only after tests prove that the harness's
|
|
446
|
+
stable tool ID maps without collision and Paid tenant scoping is enforced
|
|
447
|
+
through the owning application message/chat. The supporting table has no
|
|
448
|
+
standalone tenant column, so every read/write path must join through a
|
|
449
|
+
tenant-owned record and cross-tenant tests are mandatory.
|
|
450
|
+
|
|
451
|
+
`ruby_llm_usages` records one physical attempt and links polymorphically to the
|
|
452
|
+
application chat and optionally a message. It preserves failed/cancelled usage,
|
|
453
|
+
but its row ID and record class are private and its public instrumentation has
|
|
454
|
+
no stable attempt ID. Paid cannot use that row ID as the cross-system accounting
|
|
455
|
+
key. The harness-generated `attempt_id` must be persisted alongside Paid's
|
|
456
|
+
ledger, with an upstream-supported metadata column or mapping required before
|
|
457
|
+
adopting this table as the authoritative delivery source.
|
|
458
|
+
|
|
459
|
+
RubyLLM has no application-owned `Model`, `ToolCall`, `Usage`, or `Batch` model;
|
|
460
|
+
its record classes are explicitly implementation details. Paid keeps its chat
|
|
461
|
+
and message domain records, tenant ownership, message links, audit actor, and
|
|
462
|
+
authorization. A Rails adoption requires generated-migration review, backup,
|
|
463
|
+
production-snapshot rehearsal, backfill verification, rollback rehearsal, and
|
|
464
|
+
historical/pending-conversation tests. Reverting the gem is not a data rollback.
|
|
465
|
+
|
|
466
|
+
### Persistence alternatives
|
|
467
|
+
|
|
468
|
+
1. **Retain Paid persistence and loop over normalized transport.** Lowest
|
|
469
|
+
migration risk and the current recommendation. Implement plain Ruby
|
|
470
|
+
export/import and stable IDs in the harness.
|
|
471
|
+
2. **Use RubyLLM tool-call and usage tables selectively.** Potentially removes
|
|
472
|
+
bookkeeping, but only after stable-attempt mapping, tenant-scoped access,
|
|
473
|
+
audit, and migration tests are complete.
|
|
474
|
+
3. **Delegate the full loop and all supporting tables.** Not recommended now.
|
|
475
|
+
It does not yet demonstrate reduced maintenance across both repositories and
|
|
476
|
+
increases migration and recovery coupling.
|
|
477
|
+
|
|
478
|
+
The state investigation is therefore positive for checkpoint-based Rails
|
|
479
|
+
restoration and normalized plain Ruby reconstruction, and negative for
|
|
480
|
+
exactly-once recovery or an off-the-shelf plain Ruby export/import mechanism.
|
|
481
|
+
|
|
482
|
+
## Current harness gaps and incremental delivery
|
|
483
|
+
|
|
484
|
+
The current transports already normalize basic text, tool calls, token totals,
|
|
485
|
+
and some HTTP errors. They do not provide request-local custom headers,
|
|
486
|
+
per-request timeout/retry/cancellation, structured output, embeddings, stable
|
|
487
|
+
attempt IDs, per-attempt usage, partial terminal results, state export/import,
|
|
488
|
+
or explicit capability outcomes. `Conversation` stores in-memory history and
|
|
489
|
+
provider formatters but has no serialization contract.
|
|
490
|
+
|
|
491
|
+
Ship capabilities independently in this order:
|
|
492
|
+
|
|
493
|
+
1. common values, capability discovery, classified errors, and attempt reports;
|
|
494
|
+
2. embeddings for verified operation/provider/custom-endpoint scopes;
|
|
495
|
+
3. normalized non-streaming and streaming chat transport;
|
|
496
|
+
4. structured output for verified model/protocol combinations;
|
|
497
|
+
5. plain Ruby state round trips; and
|
|
498
|
+
6. optional Rails persistence evaluation, then loop evaluation.
|
|
499
|
+
|
|
500
|
+
Each implementation issue starts with failing contract tests for request-local
|
|
501
|
+
credential isolation, custom endpoints/headers, unsupported outcomes,
|
|
502
|
+
classified errors, bounded non-nested retries, cancellation, partial streams,
|
|
503
|
+
stable IDs, and unknown usage where applicable. Provider-specific fixtures and
|
|
504
|
+
types stay behind the harness boundary.
|
|
505
|
+
|
|
506
|
+
## Compatibility and release evidence
|
|
507
|
+
|
|
508
|
+
AgentHarness currently supports Ruby 3.2 and later and must remain usable as a
|
|
509
|
+
plain Ruby gem. RubyLLM 2.0.0 itself supports Ruby 3.1.3 and later, but adding it
|
|
510
|
+
would introduce Faraday, event-stream parsing, Schematist, Marcel, and Zeitwerk
|
|
511
|
+
runtime dependencies. A capability issue must measure and publish the resolved
|
|
512
|
+
dependency set and test the harness minimum Ruby version before adoption.
|
|
513
|
+
|
|
514
|
+
Rails and Active Record remain optional. Requiring `agent_harness` in a process
|
|
515
|
+
without Rails MUST NOT load Active Record, connect to a database, or require
|
|
516
|
+
supporting tables. Rails integration belongs behind an optional require and
|
|
517
|
+
adapter.
|
|
518
|
+
|
|
519
|
+
For every capability, release evidence must name:
|
|
520
|
+
|
|
521
|
+
- the first published, installable agent-harness version containing it;
|
|
522
|
+
- supported operation/provider/protocol/authentication combinations;
|
|
523
|
+
- upstream contract and integration test runs on the minimum Ruby version;
|
|
524
|
+
- verification of request-local secrets and absence of secret-bearing logs;
|
|
525
|
+
- retry/cancellation and attempt-accounting test evidence;
|
|
526
|
+
- the exact Paid and agent-image versions that consume the release; and
|
|
527
|
+
- retained paths and follow-up issues for combinations not migrated.
|
|
528
|
+
|
|
529
|
+
Paid issue `viamin/paid#4014` should receive this compatibility result, the
|
|
530
|
+
state-restoration conclusion, the stable-ID gap, and the per-capability release
|
|
531
|
+
evidence. Downstream adoption cannot proceed from this design issue closing or
|
|
532
|
+
from a Git tag alone.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agent-harness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.39.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- Rakefile
|
|
98
98
|
- bin/console
|
|
99
99
|
- bin/setup
|
|
100
|
+
- docs/provider-neutral-api-execution-contract.md
|
|
100
101
|
- json-2.18.1.gem
|
|
101
102
|
- lib/agent-harness.rb
|
|
102
103
|
- lib/agent_harness.rb
|