Configure built-in retries across execution paths

This commit is contained in:
2026-09-23 16:56:20 +00:00
parent ae4540822e
commit 07a131bd33
21 changed files with 441 additions and 90 deletions

View File

@@ -187,15 +187,26 @@ request.Validation = &promptkit.OutputContract{
}
```
Each repair attempt is another model call, so it can increase latency and
Each repair attempt is another model generation, so it can increase latency and
usage; `RunResult.Usage` is cumulative and `Validation.RepairAttempts` reports
calls actually started. Exhaustion still returns the final failed validation
result. `basic` validation can also repair an empty candidate, but structural
repair generations rather than transient HTTP attempts. Exhaustion still
returns the final failed validation result. `basic` validation can also repair
an empty candidate, but structural
validity is not evidence of factual or domain correctness. See the
[output-contract format reference](../formats.md#output-contract) and
[`OutputContract` GoDoc](../../types.go) for the exact budget and eligibility
rules.
The built-in client also retries eligible temporary HTTP statuses. By default,
each generation can make two additional attempts; a repair generation receives
its own budget. Set `Config.TransientRetries` to a pointer to zero for the
previous single-attempt behavior, or set `RunRequest.TransientRetries` to
override one execution. A custom HTTP transport still uses this policy;
injected `LLMClient` implementations retain their own retry policy. Application
semantic retries are separate runs and can multiply provider traffic further.
See the [provider integration contract](../integrations/openai-compatible-chat.md#transient-status-retries)
for eligible statuses, waits, timeouts, and provider timing.
### Append Already-Rendered Messages
An application can include an earlier assistant response and its own corrective
@@ -548,13 +559,13 @@ var generationErr *promptkit.GenerationError
if errors.As(err, &generationErr) {
status := generationErr.StatusCode()
message := generationErr.ProviderMessage()
_, _ = status, message // Apply application retry and presentation policy.
_, _ = status, message // Apply application presentation policy.
}
```
All provider fields are untrusted and can contain sensitive request or schema
fragments. Do not log, display, or return them without an application-specific
disclosure policy. Promptkit does not assign retry or presentation behavior.
disclosure policy. Promptkit does not assign presentation behavior.
The [`GenerationError` GoDoc](../../generation_error.go) owns the exact typed
error contract.

View File

@@ -139,6 +139,32 @@ identity and the exact transport error for `errors.Is` and `errors.As` checks.
The rendered error does not include the selected endpoint, request headers,
request content, credentials, or provider body.
## Transient Status Retries
The built-in path retries only HTTP 429, 500, 502, 503, and 504. It makes two
additional attempts by default; engine configuration or an individual run can
choose a different nonnegative count, including zero for single-attempt
behavior. The same budget applies separately to initial generation and each
structural repair generation. An injected `LLMClient` is dispatched once by
Promptkit regardless of this setting. A custom `http.Client` on the built-in
path remains subject to these retries.
Each retry uses equal jitter between half and all of an exponential local
delay, starting at 500 milliseconds and capped at 8 seconds. The first valid
`Retry-After` header value, as unsigned decimal seconds or an HTTP date, sets
a minimum not-before time measured from response receipt. Invalid values use
local delay; past dates impose no extra delay. The provider minimum is not
capped at 8 seconds. Retry waiting is cancellable and holds no active backend
permit. A caller without a deadline can wait a long time for provider timing.
The request body, selected URL, and resolved authorization are fixed across
attempts of one generation. Each attempt creates a fresh HTTP request and
receives its own configured generation and HTTP-client timeout after capacity
admission. The caller context covers every attempt and wait without a reset.
Transport failures, malformed responses, noneligible statuses, and invalid
generated content are not transient retries. A positive budget ending in
eligible statuses reports retry exhaustion with the final provider cause.
## Timeout And Cancellation
Timeouts are layered:

View File

@@ -54,32 +54,32 @@ reserve another bounded slot.
## FIFO Generation Permits
`NewClient` wraps the engine's selected internal model client after public
client adaptation or built-in client construction. Initial generation and the
default repairer receive the same wrapper. Their requests retain the same
effective backend, credential, numeric-presence metadata, and structured-output
settings, so scheduling does not change provider omission semantics between
calls.
The engine supplies one [generation coordinator](generation.md) to initial
generation and the default repairer. Their requests retain the same effective
backend, credential, numeric-presence metadata, structured-output settings,
and retry budget. Scheduling does not change provider omission semantics
between calls.
For each `Generate` call, the wrapper selects a pool from the request's
effective backend ID. An unlimited call passes directly to the next client. A
limited call acquires an active permit, invokes the next client, and defers
permit release so ordinary returns and panic unwinding both restore capacity.
For each model attempt, the coordinator selects a pool from the request's
effective backend ID. An unlimited call uses the unrestricted path. A limited
call acquires an active permit and defers release around that attempt, so
ordinary returns and panic unwinding both restore capacity.
Preparation and validation never hold an active permit.
The manager's `Acquire` operation owns that active-permit acquisition and
returns an idempotent release function. The existing wrapper delegates to it;
the internal generation coordinator uses the same operation around individual
attempts. There is one queue and one active limit per limited backend pool.
returns an idempotent release function. The coordinator uses it around
individual attempts. The earlier one-shot wrapper still delegates to the same
operation for focused internal callers and tests, but engine assembly no
longer uses it. There is one queue and one active limit per limited backend pool.
When all active permits are occupied, calls join a mutex-protected FIFO waiter
list. Releasing a permit transfers it directly to the oldest remaining waiter
before making it generally available. Pools do not order work relative to
other backend IDs.
The wrapper passes generation requests, responses, and collaborator errors
through unchanged. It owns scheduling only; the concrete model client remains
responsible for provider transport behavior. The runner, rather than the
The coordinator passes injected generation requests, responses, and errors
through unchanged. The built-in HTTP client remains responsible for provider
transport behavior. The runner, rather than the
capacity layer, sums all five usage fields from the initial response and every
completed repair response into the successful run result.
@@ -87,10 +87,10 @@ completed repair response into the successful run result.
Admission checks the caller context before reserving a slot. A call canceled
while waiting for an active permit removes its waiter under the same pool lock
used to grant permits. If cancellation removes the waiter first, the wrapped
client is not invoked. If a concurrent grant wins first, the call owns the
permit and invokes the client with the original context, allowing the client
to observe cancellation normally.
used to grant permits. If cancellation removes the waiter first, no model
attempt is dispatched. If a concurrent grant wins first, the call owns the
permit; the coordinator checks the context before dispatch and releases the
permit if cancellation has already occurred.
This grant-or-cancel decision prevents lost and double-released permits.
Admission leases and active permits are released after success, collaborator

View File

@@ -2,16 +2,17 @@
`internal/generation` coordinates one model-generation request through the
existing engine-local [capacity manager](capacity.md) and the
[built-in HTTP client](llm.md). The current engine still uses the existing
capacity wrapper; engine assembly moves to this coordinator when public retry
configuration is wired.
[built-in HTTP client](llm.md). The engine supplies the same coordinator to
initial generation and the default output repairer.
The coordinator has explicit built-in and injected-client constructors. The
built-in path prepares the provider request once, then acquires and releases an
active backend permit around each actual HTTP attempt. Only HTTP 429, 500, 502,
503, and 504 can lead to another attempt. The injected-client path acquires a
permit and invokes its client once, regardless of the internal retry budget.
The runner continues to own the logical execution's admission lease.
The runner continues to own the logical execution's admission lease. A request
can override the engine's retry budget; the runner freezes that choice for
prepared execution and passes it to each repair generation.
Retry waits hold no active permit. The local delay uses equal jitter between
half and all of a capped exponential base, beginning at 500 milliseconds and

View File

@@ -19,7 +19,7 @@ contributor workflow and validation.
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, profiles, execution targets, rendering, generation, and validation, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, and output contracts. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [endpoint invariant](../../internal/domain/endpoint.go) |
| `internal/engine` | Assembles external catalogs, Promptkit-owned profiles, repositories, registry, capacity, collaborators, repairer, and the one use-case runner for each engine instance. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#profiles-and-built-ins), [internal runner](runner.md) |
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries and cancellable backoff; engine assembly has not switched to it yet. | [Internal generation coordination](generation.md) |
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries and cancellable backoff. | [Internal generation coordination](generation.md) |
| `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) |
| `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.go) |
| `internal/jsonvalue` | Validates and deeply copies bounded JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types and rejecting cycles or excessive depth and work. | [JSON values](../../internal/jsonvalue/jsonvalue.go) |

View File

@@ -20,11 +20,11 @@ and override semantics consumed by the runner.
profiles, backend resolution, artifacts, rendering, model generation, and
validation. The internal engine supplies one immutable registry containing the
built-in backend and validated consumer additions, one engine-local run
admitter, and a model client wrapped by the same capacity manager. Validation
admitter, and a generation coordinator using the same capacity manager. Validation
plans and provider-facing schema metadata come from the validator's preparation
interface.
The internal engine supplies one default output repairer through the explicit
runner constructor, using the same capacity-wrapped client as initial
runner constructor, using the same coordinator as initial
generation. The no-repair runner constructor remains available for focused
internal callers and tests.
@@ -145,8 +145,8 @@ method must return before cancellation can take precedence over its outcome.
The admission lease covers completion-phase preparation, initial generation,
validation, every repair, and every exit. It bounds accepted work without
serializing preparation or validation behind the active-generation limit.
The wrapped model client separately acquires a FIFO active permit only around
each actual generation call.
The coordinator separately acquires a FIFO active permit only around each
actual model attempt. It releases the permit before a transient retry wait.
After a failed `basic`, JSON, or JSON Schema validation with a positive frozen
budget, the installed repairer can make a bounded corrective call. Each request
@@ -158,9 +158,10 @@ correction carries validation diagnostics as JSON data bounded to 64 KiB; the
full diagnostics remain in the validation result.
Repair receives the effective execution target, explicit numeric-presence bits,
credential, backend identity, session ID, and structured-output specification.
credential, backend identity, session ID, structured-output specification, and
frozen transient retry budget.
The same request constructor supplies those common fields to initial and repair
generation. The default repairer uses the same wrapped client as initial
generation. The default repairer uses the same coordinator as initial
generation, so each repair reacquires the selected backend's active permit
while remaining inside its original admission lease. Repair never performs a
second bounded admission, and repaired outputs use the operation's existing
@@ -218,8 +219,9 @@ behavior, hashing, generation and validation outcomes, backend propagation,
bounded repair progression, initial/repair request parity, cumulative usage,
shared initial/repair capacity, credentials and redaction, error categories,
artifact metadata, and timing. The
[capacity subsystem document](capacity.md) identifies the focused pool,
waiter, and wrapped-client tests.
[capacity subsystem document](capacity.md) identifies the focused pool and
waiter tests. The [generation document](generation.md) owns attempt and retry
coordination tests.
Changes to orchestration should continue to use the existing package
interfaces, keep request state local to an invocation, and preserve the shared

View File

@@ -309,6 +309,8 @@ and panic must not leak permits. Run focused race tests and commit.
## Stage 5 — Wire Retry Configuration Through Both Execution Paths
**Completed:** 2026-09-23.
Add public/internal configuration and request fields. Thread the copied effective
retry count through ordinary resolution, private prepared payloads,
`executePreparedRun`, `newGenerationRequest`, and `RepairRequest`. Do not add it