# Reliable Execution Implementation Plan ## Scope And Working Rules Implement the target in [reliable-execution.md](reliable-execution.md). This plan is for sequential execution by a coding agent, one numbered stage per prompt. Stages intentionally separate transport refactoring, retry behavior, lifecycle instrumentation, public integration, and consumer evidence. Do not merge stages merely because their production changes look small. Read all [repository policies](../development.md#initial-orientation), especially the [agent](../policy/agents.md), [architecture](../policy/architecture.md), [documentation](../policy/documentation.md), [testing](../policy/testing.md), and [release](../policy/release.md) policies. Preserve unrelated changes. Commit each completed, validated stage with a plain-English message and report remaining work. Do not push, tag, publish, rewrite history, or change dependencies. Licensing is complete. No new live-provider checks or release gates are needed. Use focused tests at each stage; run race tests for concurrency changes. Keep intermediate commits building, and expose public controls only when their behavior works. Update owning documentation alongside implemented behavior, not ahead of it. Exact internal helper names may change without approval; the consumer semantics and explicit choices below must not drift silently. ## Decisions Shared By All Stages ### Public Controls And Prepared Execution - Add `Config.TransientRetries *int` and `RunRequest.TransientRetries *int`. Nil selects two additional attempts at construction or inherits the engine value on a request. Zero disables retries. Reject negative values with `ErrInvalidConfig` or `ErrInvalidRequest`, respectively. Do not invent a small maximum; count actual attempts with uint64 and convert the nonnegative budget before adding the initial attempt, avoiding int overflow at MaxInt. Use overflow-safe duration arithmetic. Copy pointed values. - Validate request retry values in `Prepare`, `PrepareExecution`, and `Run`. `Prepare` performs no retries; `PrepareExecution` freezes the effective retry count privately. Do not add retry settings to profile YAML, execution targets, provider payloads, prepared metadata, or prompt hashes. - Add `RunRequest.Observer ExecutionObserver`, with `type ExecutionObserver func(ExecutionEvent)`. It has no error return and is excluded from JSON. Ordinary `Run` observes that invocation. Successful `PrepareExecution` retains the function in its private payload for the owning `RunPrepared`; `Prepare` and `PrepareExecution` do not emit execution events. No new `RunPrepared` parameters or parallel execution API are introduced. - `RunPrepared` observation starts only after successful atomic claim. Invalid, foreign, reused, discarded, or nil handles cannot supply an execution observer and emit no events. Preparation failures return their existing errors without execution events. Ordinary `Run` does observe pre-generation errors, including facade conversion errors and a nil engine if an observer was supplied. - Clear retained observers and private execution settings on claim completion or discard along with the existing private payload. Never invoke observers while holding the prepared-handle mutex. A callback is retained by identity; its closure is not cloned. Document caller synchronization responsibilities. - Injected `LLMClient` values are never automatically retried, even when configured retry values are positive. Values are still validated. A custom HTTP client used by the built-in implementation does receive built-in retries; explicit zero disables them when a transport already owns retries. ### Retry Semantics Only HTTP 429, 500, 502, 503, and 504 permit a retry. Transport failures, timeout, malformed envelopes, invalid requests, and nil collaborator responses do not. Content repair remains separate. Each repair generation gets the same configured transient budget as initial generation, never a budget shared across the run. For retry ordinal k starting at 1, use equal jitter in [base/2, base], where base is min(500 milliseconds × 2^(k-1), 8 seconds). Cap before doubling to avoid overflow. These are documented defaults, not public clock/backoff options. Internal tests can supply a clock, jitter function, and cancellable wait function through private constructors; do not change package globals or sleep in tests. Parse `Retry-After` as unsigned decimal seconds or an HTTP date via `http.ParseTime`, following [RFC 9110 section 10.2.3](https://www.rfc-editor.org/rfc/rfc9110.html#section-10.2.3). Use the first header value; trim whitespace; negative, fractional, or malformed values use local backoff. Past dates supply zero additional minimum. Measure a valid delay from response receipt, not from the end of body decoding; wait until the later of that not-before time and the local backoff deadline. Never apply the 8-second local cap to the provider minimum. Saturate unrepresentably large valid delays safely rather than wrapping or treating them as permission to retry early. Cancellation interrupts the wait; it must not busy-loop. Check the caller context before waiting, before permit acquisition, and before dispatch. Caller cancellation/deadline wins when observed at those boundaries. Do not proactively reject long provider delays; wait cancellably. A context without a deadline can wait a long time, which must be documented. Existing HTTP-client and target generation timeouts apply separately to each actual attempt after capacity admission. Retry waiting and capacity waiting use the original caller context, not a previously canceled attempt context. No new overall timeout is imposed. Never reset the caller's deadline. Freeze built-in request payload, URL, and resolved authentication once per generation sequence, before its first attempt; create a fresh HTTP request/body reader and timeout context for every attempt. An absent optional key must remain absent if the environment changes during retries. Repair is a new generation sequence and may resolve the environment again, consistent with existing calls. Do not freeze environment values at `PrepareExecution` time. When R is zero, return the ordinary first failure, not retry exhaustion. When R is positive and all R+1 attempts return eligible statuses, return exhaustion. If a later failure is noneligible or the context stops the sequence, return that failure rather than exhaustion. Count attempts actually dispatched, not waits scheduled. For one run, the bound is (R+1)×(P+1), where P is the repair budget; document the formula but do not compute it using overflow-prone `int` products. ### Error Contract Add public `ErrTransport`, `ErrMalformedResponse`, and `ErrRetriesExhausted`. Transport/malformed failures during generation also retain `ErrLLMGenerate`. Preserve standard context identities and original transport causes for `errors.Is`/`errors.As`. Do not expose new internal concrete error types as the consumer contract or stringify sensitive transport errors while mapping them. Add immutable `RetryExhaustedError` with `Attempts() uint64`, `Unwrap() error`, `Is(error) bool` matching `ErrRetriesExhausted`, and redacted `Error`/`GoString`. Map its cause recursively so `errors.As` still finds the existing public `GenerationError` and status/detail accessors. Nil receivers and zero values must be safe, consistent with existing typed errors. No stable JSON contract. Do not let the current provider-error special case in `mapPublicError` discard the exhaustion wrapper or outer validation/generation identities. Injected errors retain their identities; do not guess transport or HTTP classifications by text or structural resemblance. An injected client can use the public sentinels deliberately. A nil successful collaborator response is malformed generation. Completed structural rejection remains a normal result with failed validation; operational validation errors remain errors with no partial `RunResult`. ### Observation Values And Accounting Define public values in `observations.go`, with separate internal equivalents and facade conversion, not aliases. They have no stable serialized contract; consumers translate them into application-owned records. `ExecutionEvent` contains: - `Kind ExecutionEventKind`: constants `ExecutionStarted`, `ExecutionAttempt`, `ExecutionRetryDelay`, `ExecutionFinished` (string values `started`, `attempt`, `retry_delay`, `finished`). - `RunID string`, `Time time.Time`, and `PromptID`, `PromptVersion`, `ProfileID`, `BackendID`, `Model` string fields. Use the same RunID as a successful result. Start events for ordinary runs have no resolved identities; prepared runs may supply their already-resolved snapshot. Enrich later events as resolution succeeds. Do not label requested but unresolved values as selected values. - `Generation uint64` (1 initial, 2 first repair), `Attempt uint64` (1-based within a generation), and `Dispatched bool`. Non-attempt events use zero indexes unless describing the retry delay after a specific failed attempt. - `CapacityWait`, `GenerationDuration`, `RetryDelay`, `Duration time.Duration`. - `Outcome ExecutionOutcome`: `success`, `invalid_output`, `error`, or empty when not applicable; corresponding constants use the `ExecutionOutcome` prefix. - `Failure ExecutionFailureKind`, `StatusCode int`, `RetriesExhausted bool`. Failure constants use the `ExecutionFailure` prefix; `ExecutionFailureNone` is the zero value empty string. Other values are `canceled`, `deadline`, `transport`, `provider_status`, `malformed_response`, `capacity`, `invalid_config`, `invalid_request`, `prompt_not_found`, `profile_not_found`, `prompt_load`, `profile_load`, `artifact_load`, `prompt_render`, `validation`, `generation`, `other`. Missing credentials/profile selection use invalid_request. Classify context identities first, then specific causes before broad operation categories. Exhaustion is a separate flag, not a replacement for provider_status. - `Usage ObservedTokenUsage` and terminal `Attempts`, `TransientRetries`, `RepairCalls uint64`. Nonterminal aggregate counters are zero. `ObservedTokenUsage` has the existing five token field names, each an `ObservedTokenCount{Value int; Known bool; Complete bool}`. An attempt field is known/complete exactly when the provider supplied that field as a non-null integer. An aggregate Value sums known contributions, Known means at least one known contribution, and Complete means every dispatched attempt reported that field. For zero dispatched attempts use known complete zero totals. Preserve reported values without deriving totals or reconciling fields. Unknown fields have Value zero. Counts on non-dispatched attempts are unknown and excluded from usage aggregation. This distinguishes partial accounting from complete zero. Add optional `GenerateResponse.UsagePresence *TokenUsagePresence` with JSON tag `usage_presence,omitempty`; `TokenUsagePresence` contains five bool fields with the corresponding snake_case JSON names. For nil from legacy injected clients, nonzero fields are known and zero fields are unknown: the old interface cannot distinguish omitted zero from measured zero. An explicit presence value permits known zeros and governs all five fields. Copy it on conversion. Existing result accounting still sums all Usage values regardless of this observation metadata; `TokenUsage` and existing `RunResult` JSON remain unchanged. New GenerateResponse JSON round-trips the additive field and omits nil. The built-in adapter always supplies explicit presence; absent/null wire counts are unknown. Emit one attempt-completion event per acquisition attempt, including interrupted capacity waiting with Dispatched=false. GenerationDuration covers dispatch through response decoding/closure, excludes capacity/backoff/callback time. CapacityWait covers acquiring a permit, and is zero for the unlimited path. Release the active permit before invoking observers. Dispatched means invoking the prepared HTTP attempt or injected Generate, not a proven network packet or billable request; hidden HTTP redirects/transport retries are not observable. Emit retry-delay completion after each actual wait, including interrupted waits, with elapsed RetryDelay and the failed attempt indexes. No delay event when no wait starts. Emit terminal totals once on each normal return, including errors, after admission release. Attempts counts dispatched calls; TransientRetries counts dispatched attempts numbered above 1; RepairCalls counts generations above 1 with at least one dispatched attempt. Do not count scheduled work. Terminal phase-duration fields sum their respective measured phases; Duration is wall-clock operation time and can additionally include preparation, validation, callbacks, and other work. Do not add terminal usage to attempt usage. Attempt success means generation succeeded, not that later output validation accepted it. Synchronous callbacks are ordered per operation, may overlap across operations, and cannot return errors. Event values contain no mutable containers or error objects. Never include endpoint URLs, headers, keys, session values, bodies, validation diagnostics, or provider text. Metadata identifiers can still be sensitive. Callback panics propagate; defer cleanup before first callback after resource acquisition. Do not promise terminal events after panics or process termination and do not invoke further callbacks while unwinding a panic. Callbacks must return for the operation to progress; cancellation cannot forcibly interrupt consumer code. Warn against blocking on work requiring the same run's admission slot. No asynchronous delivery or durable history. ## Stage 1 — Expose Missing Generation Failure Categories **Completed:** 2026-09-23. Read `errors.go`, `generation_error.go`, `llm_adapter.go`, the LLM error types, and public error-boundary tests. Add public transport/malformed sentinels and map current built-in errors while preserving cause identity and redacted text. Classify actual request execution failures as transport, not unrelated request encoding failures merely sharing the old broad ErrRequestFailed sentinel. Normalize nil successful injected responses into the malformed category. Do not add retries or observers yet. Retain existing provider-status and validation behavior. Add the error GoDoc and a short consumer-guide classification update. Verify public tests for transport causes, caller cancellation/deadline, malformed framing/choices, nil response, provider detail redaction, and injected arbitrary errors. Reuse current fixtures; avoid duplicating the entire transport matrix. Run `go test ./...`. Commit when mappings and existing tests pass. ## Stage 2 — Preserve Usage Presence Through The Adapter **Completed:** 2026-09-23. Read wire response decoding, domain/public response types, facade conversion, and JSON contract tests. Add the presence types and optional public response field above. Convert wire usage counts to pointer-aware decoding, retaining existing integer values and response validation behavior. Explicit null and omission are unknown; numeric zero is known. Invalid numeric types retain current malformed-response handling. Apply the nil-presence fallback above only to observation metadata, never to existing successful-result accounting. Test absent usage, partial nested usage, nulls, zero, and all five reported fields at the decoder boundary. Test public optional-field JSON round trips and defensive copying once at the facade. Existing successful RunResult totals and JSON must be unchanged. Document injected-client presence semantics in GoDoc. No observation API yet. Validate affected packages and commit. ## Stage 3 — Separate Frozen HTTP Preparation From One Attempt **Completed:** 2026-09-23. Refactor `internal/llm/OpenAICompatibleClient` without changing its one-shot `Generate` behavior. Add an internal `PrepareGeneration` operation that validates and encodes one request, resolves credentials once, and returns private frozen state with an `Execute(ctx)` method. It creates a fresh body/request/deadline on every Execute and closes every response before returning. Return an internal attempt outcome containing response, error, HTTP status, usage plus presence, and parsed provider not-before timing. Preserve known usage from a strictly decoded successful envelope even if missing choices/content subsequently make it malformed; do not salvage data from invalid framing or non-success error bodies. Preserve current body-size limits and redaction. The existing Generate method delegates once and returns no partial response on error. After failed body reading/decoding, retain an observed attempt-context cancellation/deadline instead of hiding it behind malformed-response formatting. Timing/header data remains internal, not provider-error strings. Implement Retry-After parsing here, but no sleeping or retry loop. Test frozen payload/authentication across repeated Execute calls, including an initially absent optional key; independent per-attempt timeout contexts; body closure; usage on malformed choices; and header parsing with an explicit receipt time. Use separate Execute contexts to verify a canceled attempt does not poison the next call. All existing wire tests must pass. Commit this refactoring separately. ## Stage 4 — Implement The Internal Retry And Capacity Coordinator **Completed:** 2026-09-23. Create `internal/generation`, depending on domain, llm, and capacity. It implements the existing `llm.Client` consumed by the runner and repairer. For built-in generation it prepares once through Stage 3, then loops over capacity-controlled Execute calls. For injected generation it dispatches once. Constructor arguments explicitly select the path; do not infer retry eligibility from arbitrary errors. Expose an internal Manager active-permit acquisition method returning an idempotent release function. Reuse pool logic; never build another queue. Keep the current capacity client as a delegating one-shot wrapper until engine wiring moves, sharing that acquisition primitive. The coordinator defers permit release inside each attempt; no permit survives a wait or callback. Admission remains runner-owned. Invalid frozen-request preparation dispatches nothing. Implement the exact delay/context/count semantics above with private test seams. Introduce an internal exhaustion wrapper retaining the cause and attempt count. The coordinator accepts a resolved retry count on the internal GenerateRequest; it has no public API or engine behavior change yet. Repair propagation arrives in Stage 5. Leave observation hooks for later stages rather than adding no-op public callbacks now. Tests own the retry status table, R=0, exhaustion, a noneligible later failure, context interruption, provider minimum versus jitter, overflow-safe backoff, frozen absent/present credentials, and independent attempt deadlines. A blocked backoff must allow another call to acquire the only active permit. Cancellation and panic must not leak permits. Run focused race tests and commit. ## Stage 5 — Wire Retry Configuration Through Both Execution Paths 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 to public GenerateRequest or provider settings: injected clients own their policy. Keep one shared resolution helper for default/override/negative validation. In `internal/engine.New`, assemble the generation coordinator around the selected built-in or injected client and shared manager. Do not wrap it again in capacity.NewClient: that would hold a permit over waits or deadlock reacquisition. Initial generation and default repairer must receive the same coordinator. Remove obsolete capacity-wrapper machinery/tests only when its distinct behavior is covered by the coordinator and manager tests; do not retain two live schedulers. Add the public exhaustion type and recursive error mapping. Update old tests expecting one eligible HTTP failure to set explicit zero where their purpose is not retry behavior, rather than making unrelated tests wait through backoff. Document the newly active controls, default two retries, and per-attempt timeout semantics now. Add public tests for request/engine precedence, copied pointers, prepared freezing, repair budget multiplication, exhaustion cause access, and injected-client single dispatch. Run full tests and focused race tests; commit. ## Stage 6 — Build The Internal Observation Recorder Create `internal/observation` for invocation-local counters, presence-aware aggregation, and synchronous dispatch. Internal event/value counterparts and the callback function type belong in domain so domain requests/payloads can carry them without importing the recorder package. The recorder depends only on domain/standard library, not provider or root types. No global map, history slice, mutex held across callbacks, goroutine, or background queue is needed. Keep outcome classification at owning boundaries; recorder consumes normalized failure metadata, never raw error strings. Move/reuse run-ID generation here so observation and successful results have one identity owner. Provide start/update/attempt/delay/finish operations and a private context key to pass the invocation-local recorder through the existing internal client interfaces. Context propagation is solely internal plumbing, not public control policy. A nil observer avoids event delivery, but counts needed for errors and existing results must still work. If run-ID creation fails, report an error terminal event with an empty RunID rather than invent a valid identifier. Test phase sums, dispatched-only counters, partial/unknown/zero usage, no double counting, callback order, retained-value independence, and panic propagation. Do not promise event delivery across panics. There is no public observer or runner wiring in this stage. Update the internal inventory when the package lands. ## Stage 7 — Instrument Attempts And Retry Delays Connect the coordinator to the internal recorder through context. Record measured capacity wait even on interrupted acquisition, actual dispatch and outcome, usage from Stage 3, and completed/canceled backoff. Release the active permit before callback delivery. Collect injected response usage using presence from Stage 2; arbitrary injected failures have unknown usage. Error response bodies are not mined for usage. Do not add an event for request freezing failure: the terminal operation will report it without pretending a dispatch happened. A completed attempt callback that cancels context must prevent another attempt. Measure phase end before its callback so observer work is not reported as provider time. Feed normalized HTTP and context classifications without exposing sensitive strings. Tests cover retry then success, exhaustion, interrupted wait, canceled capacity, success then later repair failure (using internal recorder plumbing), legacy injected usage and explicit unknown fields, and callbacks outside permits/locks. Use synchronization rather than elapsed-time thresholds. Run race tests; commit. ## Stage 8 — Add Ordinary-Run Lifecycle And Public Observation API Add public observation types/conversion and RunRequest.Observer. The facade adapts the function and delegates bookkeeping to the internal recorder. Start ordinary Run observation before request conversion and nil-engine rejection; finish after public error mapping so the terminal classification matches consumer errors. Use the recorder's RunID/start in the runner instead of generating another ID. Without an observer preserve existing Result timing conventions; with an observer its Duration includes the facade boundary and can be slightly longer. Enrich resolved identities at the runner's resolution boundary, before admission; do not copy unresolved input strings into selected fields. Generation indexes are passed through initial and repair paths. Ensure final totals include completed generation usage before validation or repair errors. Emit finished only on normal return, after all admission cleanup; do not use a defer that calls user callbacks during panic unwinding. Keep `Run` signatures and no-partial-result behavior. Tests cover source/conversion failure, nil engine, admission rejection, validation operational failure, valid/invalid/skipped validation, success followed by repair failure, matching RunID, redaction, and observer panic cleanup. Verify callback mutation/retention cannot alter execution state. Update public GoDoc and consumer guidance for ordinary observations only; run root and internal race tests; commit. ## Stage 9 — Observe Prepared Execution Without Weakening Its Lifecycle Retain the adapted observer with the effective retries in the private prepared payload. Claim atomically before starting observation, release the claim mutex, install payload cleanup, then emit started. Preparation emits nothing; successful claim followed by credential failure, capacity rejection, or cancellation still emits finished on a normal return. Invalid/reused/foreign handles emit nothing and must neither invoke nor discard another execution's callback. Use the same recorder, identity enrichment, failure normalization, and terminal accounting as ordinary execution. Keep callback work outside handle and manager locks. Discard clears observer references without calling them. Concurrent calls using one handle result in one executing observation sequence, not duplicate runs. Add focused prepared public tests for frozen retry choice/observer, discarded and foreign handles, claim races, no preparation events, later failures with usage, and panic cleanup. Preserve existing preparation/result timing distinctions. Document the prepared observer lifetime and exclusions. Run race tests; commit. ## Stage 10 — Verify Cross-Cutting Public Contracts Review assembled behavior through the public facade using controlled HTTP transports and small synthetic fixtures. Add only missing integration evidence: - Two profiles sharing one finite backend coordinate initial, retry, and repair traffic. Backoff frees the active slot but not logical admission. Different backend pools remain independent; unlimited/endpoint-only paths still work. - A sequence of transient failure, successful invalid output, repair transient failure, and successful valid output produces consistent counts, known partial usage, result usage, events, and error identities. Exercise exhaustion and cancellation as focused variants, not a Cartesian product. - Frozen request/authentication and prepared source immutability survive retries. Noneligible errors never replay. Injected client attempts remain opaque. - Safe default error/event formatting and JSON never disclose credential markers, request bodies, provider message markers, or retained callback internals. Do not require fixed callback timing, random samples, exact error wording, or internal helper-call sequences. Fix interaction defects in their owning layer. Run `go test -race -vet=off ./...` and `go vet ./...`; commit the verified scope. ## Stage 11 — Add The Offline Proposal And Assessment Example Add `examples/go-library/observe` with embedded prompt/schema assets, two application-owned profiles sharing a finite custom backend, and one engine. Use a controlled RoundTripper under the built-in client: return one eligible status before a valid structured proposal, then a structurally valid assessment that the application can accept/reject independently. Collect metadata events and print a compact structured summary, including actual attempt/retry counts and assessment outcome. Do not log prompt/response bodies or provider detail. Accept the real short default backoff in this executable demonstration; unit tests still use controlled waiting. Do not add public timing controls for it. Keep cancellation/exhaustion/schema-invalid branch coverage in focused tests from earlier stages, not a large example switchboard. Add this example once to the existing release-check script with semantic output assertions; do not run a redundant second integration suite there. Update the development guide, testing policy's example inventory, and internal overview accordingly. Run the example and its relevant tests; commit. ## Stage 12 — Complete Documentation And Final Validation Reconcile GoDoc, consumer error/observer/retry guidance, provider integration docs, and internal runner/LLM/capacity/generation/observation documentation with the implemented design. Remove claims that no retries exist or that usage can only be learned on success. Clearly distinguish observations from complete cost accounting and explain nil legacy injected usage presence. Document single-attempt migration via explicit zero, multiplication with repair and application semantic retries, provider-delay/caller-deadline behavior, callback lifetime/concurrency/panics, and no-partial-result compatibility. Do not copy the same field reference into multiple documents. Link the new example. Update Audita's wishlist status to implemented where verified. Do not invent a release version or tag; versioned release notes and completed-roadmap retirement belong to the later authorized release-preparation task. Run `./scripts/release-check.sh` once for the final implementation, inspect the diff for accidental contracts, dependencies, or artifacts, and verify no public signature exposes internal types. Record results and any real remaining gaps. Commit the final coherent documentation/integration work; report readiness for review without publishing or waiting for CI. ## Open Questions None requiring a maintainer decision remain. The approved roadmap policies and the explicit API, accounting, timing, and lifecycle choices above are the basis for this implementation. If implementation reveals a conflict with an existing explicit contract, report the concrete conflict rather than silently adding a restriction or weakening that contract.