# Structured Capacity Errors Implementation Plan **Status:** Ready for implementation. ## Purpose This document is the decision-complete implementation plan for [structured capacity errors](capacity-errors.md). It is written for a gpt-5.6-terra coding agent that will implement each stage in order. The feature roadmap owns the motivation, public intent, compatibility policy, security boundary, non-goals, and target end state. This plan owns the fixed design, file-level work, implementation sequence, test ownership, documentation updates, validation commands, and completion gates. ## Implementation Rules - Complete the stages in order. Keep the repository compiling and the focused tests passing at every stage boundary. - Preserve unrelated working-tree changes. In particular, retain the accepted feature roadmap and its existing future-catalog and downstream-wishlist edits. - Follow every policy under `docs/policy/`, the task-specific reading guide in `docs/development.md`, and the accepted behavior in `capacity-errors.md`. - Keep the supported error API in the root `promptkit` package. Internal capacity and use-case error values must not become consumer dependencies. - Do not parse error strings. Carry the selected backend ID in an internal typed error and translate it explicitly at the root facade. - Do not change capacity policy, admission ordering, limits, queueing, FIFO generation scheduling, lease lifetime, cancellation behavior, backend selection, prepared-handle lifecycle, or model-client classification. - Do not classify provider throttling, HTTP 429 responses, quota failures, or injected-client errors as engine admission rejection. - Preserve `errors.Is(err, ErrCapacityExceeded)` while adding discovery as `*CapacityError` through `errors.As`. - Return a fresh public typed error for each rejected operation. Do not retain a request, pool, registry, endpoint, credential, execution target, or live capacity state in either typed error. - Add only `BackendID` to the public type. Do not add limits, counts, queue depth, retry timing, retryability, transport status, provider state, or stable JSON. - Keep tests lean and behavior-focused. Extend the existing internal admission, public capacity, prepared-execution, and root error-boundary tests instead of creating a parallel test framework. - Update exact public contracts in declarations and GoDoc. Update current-state consumer and internal documentation only after the implementation exists. - Do not add release notes, change a module version, create a release, commit, or tag as part of this work. ## Fixed Design ### Internal Admission Error Add `internal/usecase/capacity_error.go` with this internal boundary type: ```go // CapacityError identifies bounded admission rejected for one selected // backend. type CapacityError struct { BackendID string } func (e *CapacityError) Error() string func (e *CapacityError) Unwrap() error ``` Although exported from an `internal` package so the root facade can recognize it, this is not a public consumer API. Its methods have these fixed semantics: - `Error` returns concise diagnostic wording that includes a quoted nonblank backend ID and the generic internal capacity classification; - a nil receiver or blank `BackendID` produces only the generic internal capacity wording and does not panic; - `Unwrap` always returns `capacity.ErrCapacityExceeded`, including for a nil receiver; and - the value contains no cause field, request reference, capacity-manager reference, or other structured data. Update `Runner.admitRun` in `internal/usecase/runner.go`: 1. retain the existing nil-admitter unlimited fallback; 2. call `RunAdmitter.Admit` exactly once with the selected backend ID; 3. when the returned error matches `capacity.ErrCapacityExceeded` and `backendID` is nonblank, return a newly allocated `&CapacityError{BackendID: backendID}`; 4. when a capacity error is returned for a blank backend ID by an invalid or test-only collaborator, pass that error through rather than manufacturing a structured value that violates the nonblank-ID guarantee; 5. pass every non-capacity error through unchanged; and 6. preserve the release function unchanged on success. Do not move structured identity into `internal/capacity.Manager`. The use-case boundary already knows the effective selected backend used by both `Run` and `RunPrepared`, and it also normalizes any conforming `RunAdmitter` implementation into the same error contract. The capacity package continues to own only its generic internal sentinel and scheduling state. Both ordinary and prepared execution already call `admitRun`; do not add separate wrapping logic to `Run` or `RunPrepared`. ### Public Error Type Add a root file named `capacity_error.go` containing: ```go // CapacityError reports bounded admission rejected for a selected backend. type CapacityError struct { BackendID string } func (e *CapacityError) Error() string func (e *CapacityError) Unwrap() error ``` The declaration and method GoDoc must establish: - engine-produced values identify only rejection at Promptkit's bounded `Run` or `RunPrepared` admission boundary; - `BackendID` is the normalized registered backend ID used for routing and capacity, and endpoint overrides do not change it; - every engine-produced value is nonnil and has a nonblank `BackendID`; - provider errors, active-generation waiting, and caller cancellation are not represented by this type; - `Error` wording is diagnostic and not a parsing contract; - `Unwrap` returns `ErrCapacityExceeded`, so `errors.Is` and `errors.As` can be used together; - a nil receiver and the zero value remain safe and unwrap to `ErrCapacityExceeded`, but a consumer-constructed value is not evidence that an engine rejected work; - the type and its default Go encoding have no stable JSON contract; and - consumers own returned values and may mutate `BackendID` without affecting engine state or another error. Implement `Error` without exposing anything other than the public field and the generic sentinel wording. For a nil receiver or blank field, return `ErrCapacityExceeded.Error()`. Otherwise include the backend ID with `%q`. Implement `Unwrap` as an unconditional return of `ErrCapacityExceeded`. Do not add an exported constructor, custom formatter, `Is` method, JSON tags, `MarshalJSON`, or `UnmarshalJSON`. Direct equality with `ErrCapacityExceeded` is not a supported contract. ### Root Translation Update `mapPublicError` in `errors.go` before its general `publicErrorFor` mapping: 1. use `errors.As` to find an internal `*usecase.CapacityError`; 2. require the matched pointer to be nonnil and its `BackendID` to be nonblank; 3. return a newly allocated public `&CapacityError{BackendID: internalCapacityError.BackendID}` directly; and 4. otherwise continue through the existing general mapping. Returning the public value directly is deliberate. It prevents the internal typed error and internal sentinel from remaining in the returned error chain, while the public value's `Unwrap` supplies the supported public sentinel. Copy only the string field; do not retain the internal error. Leave the existing `capacity.ErrCapacityExceeded` case in `publicErrorFor`. It remains a defensive compatibility fallback for an unstructured internal capacity error. No valid `Run` or `RunPrepared` capacity rejection should take that fallback after this feature is implemented. Do not reorder unrelated error categories. In particular, generation failures remain `ErrLLMGenerate`, and the mapper must not infer admission rejection from public sentinel text, provider errors, status codes, or arbitrary errors that happen to expose a backend field. ### Operation Contracts Update the existing declarations and GoDoc without duplicating the full type contract: - the `ErrCapacityExceeded` GoDoc in `engine.go` remains the broad classification contract and points consumers to `CapacityError` for the selected backend ID; - `Engine.Run` states that engine admission rejection is discoverable as `*CapacityError` and still matches `ErrCapacityExceeded`; - `Engine.RunPrepared` states the same and retains its one-attempt handle semantics; - `doc.go` adds error values to its unstable-JSON category, lists `CapacityError` there, and notes that returned structured errors are caller-owned; and - no operation other than `Run` and `RunPrepared` claims it can return this type. Do not change method signatures or add the type to any stable JSON list. ### Error And Identity Boundaries The implementation must preserve all of these distinctions: | Condition | `errors.Is` identity | `errors.As` to `*CapacityError` | | --- | --- | --- | | Limited selected backend has no admission slot | `ErrCapacityExceeded` | Yes, with selected backend ID | | Context is already done when admission checks it | Context error | No | | Waiting for an active generation permit is canceled | `ErrLLMGenerate` and context error | No | | Provider or injected client returns throttling or quota failure | `ErrLLMGenerate` and documented collaborator identity | No | | Invalid request, profile, credential, artifact, render, or validation failure | Existing category | No | | Unlimited backend or endpoint-only profile | No admission rejection | No | For an ordinary `Run`, the ID is the effective registered backend selected during preparation. For `RunPrepared`, it is the backend frozen in the claimed handle. An endpoint override changes only the endpoint and must not change the reported ID. ### Test Ownership Extend existing tests at their current ownership boundaries. `internal/capacity/manager_test.go` continues to own admission mechanics. Strengthen `TestManagerAdmissionHonorsContextAndUnlimitedBackends` so the limited pool is full before the canceled admission attempt. This protects the existing rule that cancellation wins over capacity rejection without adding a new overlapping test. `internal/usecase/runner_test.go` owns attachment of selected identity. Update `TestRunnerAdmissionFailureSkipsCompletionCollaborators` so: - the capacity case uses `errors.As` to obtain the internal `*CapacityError`; - its `BackendID` is exactly `"custom"`; - the error still matches `capacity.ErrCapacityExceeded`; - cancellation does not produce an internal `*CapacityError`; and - the existing assertions about no partial result, no recategorization, the admitted backend ID, and skipped collaborators remain. Replace the current string-content assertion with the structured assertion. Do not test exact diagnostic wording. `errors_internal_test.go` owns root translation. Add a focused test that maps an internal `*usecase.CapacityError` and proves: - the result is a public `*CapacityError` with the copied backend ID; - it matches public `ErrCapacityExceeded`; - it does not match unrelated public categories; - it no longer exposes the internal typed error through `errors.As`; and - mutating the source internal error after mapping does not alter the public value. Retain the existing cancellation-preservation test. `capacity_contract_test.go` owns assembled public `Run` behavior. Extend `TestEngineRejectsRunBeforeCompletionWhenAdmissionIsFull` to prove: - no partial result is returned; - `errors.Is(err, promptkit.ErrCapacityExceeded)` still succeeds; - `errors.As` obtains a nonnil `*promptkit.CapacityError`; - `BackendID` is `"limited"` even though the rejected request overrides its endpoint; - unrelated public categories do not match; - mutating the returned error cannot alter a subsequent independently rejected call or its backend ID; and - no completion collaborator or model client is invoked for rejected calls. In the same established full-pool setup, add a canceled-context assertion before releasing the admitted run. It must match `context.Canceled`, must not match `ErrCapacityExceeded`, and must not be discoverable as `*promptkit.CapacityError`. This is the root contract counterpart to the manager precedence test. Extend `TestCapacityExceededSentinelContract` to cover the public type's zero and nil receiver behavior without asserting exact diagnostic wording: - both safely match `ErrCapacityExceeded`; - an ordinary populated value is discoverable by `errors.As`; and - neither the sentinel nor the typed value matches unrelated categories. `prepared_execution_contract_test.go` owns the public `RunPrepared` boundary. In the capacity-rejection section of `TestPreparedExecutionCredentialCapacityAndTimingBoundaries`, assert that `errors.As` obtains `*promptkit.CapacityError` with backend ID `"limited"`. Retain the existing assertions that the result is nil, the public sentinel matches, the handle is consumed, and details remain available. Do not duplicate manager limit matrices, generation FIFO tests, backend registration tests, or provider-client failure matrices. Existing tests already own those behaviors. ### Documentation Ownership After the code and contract tests pass: - update `docs/consumers/pkg-promptkit.md` in **Handle Errors** with a concise `errors.As` example that extracts `BackendID` while retaining the existing `errors.Is` guidance and application-owned retry policy; - update `docs/internal/runner.md` to describe the internal typed attachment and root translation without reproducing the public API contract; - update `docs/internal/capacity.md` to clarify that the manager still emits only its internal sentinel, while the runner attaches selected identity and the facade translates it; - update `docs/internal/overview.md` only enough to include the root typed capacity error in the facade responsibility; and - update `doc.go` and `engine.go` with the exact public contract described above. Do not update `docs/formats.md`, the OpenAI-compatible integration contract, backend configuration GoDoc, README, or release notes. This feature changes no file format, provider wire request, backend configuration, project orientation, or released-version record. When implementation is complete: - change `docs/roadmap/capacity-errors.md` to `**Status:** Complete.`; - change `docs/roadmap/future.md` so it no longer describes structured capacity errors as active planning and simply records that no ideas await selection; - change both downstream wishlist dispositions from accepted planning to implemented behavior, linking to the consumer guide's **Handle Errors** section rather than duplicating the contract; and - change this document to `**Status:** Complete.` The feature roadmap already contains no staged or prompt-level implementation language. Do not add such language to it when changing its status. ## Stage 1: Carry Structured Identity Across Internal Admission ### Objective Replace diagnostic-string-only backend context with a typed internal admission error while preserving capacity mechanics and cancellation precedence. ### Implementation Prompt Implement only Stage 1 of `docs/roadmap/implementation.md`. 1. Add `internal/usecase/capacity_error.go` with the exact internal type and method semantics in **Fixed Design**. 2. Update `Runner.admitRun` to create one fresh internal typed error for a nonblank selected backend when the admitter returns the internal capacity sentinel. 3. Update the existing runner admission-failure test to assert typed identity instead of inspecting diagnostic text. 4. Strengthen the existing capacity-manager context test so cancellation is checked while the limited pool is already full. 5. Run the focused formatting and validation below. Do not modify the root public API, root mapper, capacity-manager production code, public contract tests, or current-state documentation in this stage. ### Focused Validation Run from the repository root: ```sh gofmt -w internal/usecase/capacity_error.go \ internal/usecase/runner.go \ internal/usecase/runner_test.go \ internal/capacity/manager_test.go go test ./internal/capacity ./internal/usecase git diff --check ``` ### Completion Gate Stage 1 is complete only when: - capacity rejection from `admitRun` is discoverable as the internal `*usecase.CapacityError`; - its ID comes from the effective backend passed to admission; - both `Run` and `RunPrepared` use the shared boundary without duplicate wrapping; - `errors.Is` still reaches `capacity.ErrCapacityExceeded`; - cancellation and other admission errors remain untyped and unchanged; - capacity scheduling production code is untouched; and - focused tests and whitespace checks pass. ## Stage 2: Expose And Protect The Public Error Contract ### Objective Add the minimal public typed error, translate the internal value without leaking it, and protect ordinary and prepared consumer behavior. ### Implementation Prompt Implement only Stage 2 of `docs/roadmap/implementation.md` after Stage 1 satisfies its completion gate. 1. Add root `capacity_error.go` with the exact public type, methods, and GoDoc in **Fixed Design**. 2. Update `errors.go` to translate a nonblank internal typed error into a fresh public value before general sentinel mapping. 3. Preserve the existing unstructured capacity fallback and all unrelated error-mapping order. 4. Update `engine.go` and `doc.go` with the exact operation, ownership, and unstable-JSON contracts. 5. Add the focused root translation test. 6. Extend the existing external-package `Run`, sentinel, cancellation, and `RunPrepared` contract assertions described under **Test Ownership**. 7. Run the focused formatting and validation below. Do not change admission policy, add provider classification, introduce a constructor or serialization contract, or update durable prose documentation in this stage. ### Focused Validation Run from the repository root: ```sh gofmt -w capacity_error.go errors.go engine.go doc.go \ errors_internal_test.go capacity_contract_test.go \ prepared_execution_contract_test.go go test . -run \ 'TestMapPublicError|TestEngineRejectsRunBeforeCompletionWhenAdmissionIsFull|TestCapacityExceededSentinelContract|TestPreparedExecutionCredentialCapacityAndTimingBoundaries' go test . git diff --check ``` ### Completion Gate Stage 2 is complete only when: - every assembled `Run` and `RunPrepared` admission rejection is discoverable as a public `*CapacityError` with the selected backend ID; - the public error unwraps only to `ErrCapacityExceeded` and does not retain the internal typed error; - existing broad `errors.Is` handling remains valid; - endpoint overrides do not change the reported ID; - cancellation at a full pool remains a context error and not a capacity error; - caller mutation cannot affect another rejection or engine state; - provider, generation, and unrelated error mappings remain unchanged; and - focused and complete root-package tests pass. ## Stage 3: Update Canonical Documentation And Validate ### Objective Make the implemented structured error discoverable, reconcile temporary planning state, and complete repository-wide validation. ### Implementation Prompt Implement only Stage 3 of `docs/roadmap/implementation.md` after Stages 1 and 2 satisfy their completion gates. 1. Update the consumer and internal documents listed under **Documentation Ownership**. Keep exact API guarantees in GoDoc and use prose documents for task guidance and implementation boundaries. 2. Update the feature-roadmap, future-catalog, implementation-plan, and downstream-wishlist statuses and links exactly as specified above. 3. Follow every added or changed Markdown link and confirm its file and heading target. 4. Run the full validation sequence. 5. Inspect the complete diff for scope, sensitive data, and repository hygiene. 6. Only after every check passes, leave both roadmap statuses as `Complete` and rerun `git diff --check`. Do not add a release document, new example, new public package, retry policy, transport mapping, or duplicate API reference. ### Full Validation Run from the repository root: ```sh gofmt -w internal/usecase/capacity_error.go \ internal/usecase/runner.go \ internal/usecase/runner_test.go \ internal/capacity/manager_test.go \ capacity_error.go errors.go engine.go doc.go \ errors_internal_test.go capacity_contract_test.go \ prepared_execution_contract_test.go gofmt -l $(git ls-files '*.go') go test ./... go test -race ./... go vet ./... go build ./... go run ./examples/go-library/prepare git diff --check git status --short ``` The `gofmt -l` command must print no paths. The maintained example must remain offline and require no real credential or provider. Inspect the final state and confirm: - only files required by this feature and pre-existing user changes are present; - no credential, private source content, endpoint, workspace file, local replacement, generated binary, or unrelated formatting change was added; - the root declaration and GoDoc own the exact public contract; - consumer guidance summarizes the workflow and links to the canonical API; - internal documents describe responsibility without redefining the public contract; - no current-state document claims Promptkit owns retry, backoff, transport, logging, or metrics policy; - no roadmap retains staged language outside this implementation plan; and - no release, commit, or tag was created. ### Completion Gate Implementation is complete only when: - every Stage 1 and Stage 2 gate remains satisfied; - ordinary and race-enabled tests pass; - vet, build, formatting, the offline example, Markdown links, and whitespace checks pass; - public, consumer, and internal documentation agree on the implemented boundary; - future-catalog and downstream-wishlist dispositions no longer describe the feature as pending; - both roadmap statuses are `Complete`; - the working tree contains no unintended files or changes; and - the repository is ready for maintainer review without a commit or release having been created by this plan. ## Open Questions None. The accepted feature roadmap and fixed design above fully specify the implementation boundary.