22 KiB
Structured Capacity Errors Implementation Plan
Status: Ready for implementation.
Purpose
This document is the decision-complete implementation plan for structured capacity errors. 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 indocs/development.md, and the accepted behavior incapacity-errors.md. - Keep the supported error API in the root
promptkitpackage. 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*CapacityErrorthrougherrors.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
BackendIDto 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:
// 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:
Errorreturns concise diagnostic wording that includes a quoted nonblank backend ID and the generic internal capacity classification;- a nil receiver or blank
BackendIDproduces only the generic internal capacity wording and does not panic; Unwrapalways returnscapacity.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:
- retain the existing nil-admitter unlimited fallback;
- call
RunAdmitter.Admitexactly once with the selected backend ID; - when the returned error matches
capacity.ErrCapacityExceededandbackendIDis nonblank, return a newly allocated&CapacityError{BackendID: backendID}; - 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;
- pass every non-capacity error through unchanged; and
- 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:
// 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
RunorRunPreparedadmission boundary; BackendIDis 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;
Errorwording is diagnostic and not a parsing contract;UnwrapreturnsErrCapacityExceeded, soerrors.Isanderrors.Ascan 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
BackendIDwithout 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:
- use
errors.Asto find an internal*usecase.CapacityError; - require the matched pointer to be nonnil and its
BackendIDto be nonblank; - return a newly allocated public
&CapacityError{BackendID: internalCapacityError.BackendID}directly; and - 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
ErrCapacityExceededGoDoc inengine.goremains the broad classification contract and points consumers toCapacityErrorfor the selected backend ID; Engine.Runstates that engine admission rejection is discoverable as*CapacityErrorand still matchesErrCapacityExceeded;Engine.RunPreparedstates the same and retains its one-attempt handle semantics;doc.goadds error values to its unstable-JSON category, listsCapacityErrorthere, and notes that returned structured errors are caller-owned; and- no operation other than
RunandRunPreparedclaims 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.Asto obtain the internal*CapacityError; - its
BackendIDis 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
*CapacityErrorwith 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.Asobtains a nonnil*promptkit.CapacityError;BackendIDis"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.mdin Handle Errors with a conciseerrors.Asexample that extractsBackendIDwhile retaining the existingerrors.Isguidance and application-owned retry policy; - update
docs/internal/runner.mdto describe the internal typed attachment and root translation without reproducing the public API contract; - update
docs/internal/capacity.mdto 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.mdonly enough to include the root typed capacity error in the facade responsibility; and - update
doc.goandengine.gowith 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.mdto**Status:** Complete.; - change
docs/roadmap/future.mdso 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.
- Add
internal/usecase/capacity_error.gowith the exact internal type and method semantics in Fixed Design. - Update
Runner.admitRunto create one fresh internal typed error for a nonblank selected backend when the admitter returns the internal capacity sentinel. - Update the existing runner admission-failure test to assert typed identity instead of inspecting diagnostic text.
- Strengthen the existing capacity-manager context test so cancellation is checked while the limited pool is already full.
- 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:
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
admitRunis discoverable as the internal*usecase.CapacityError; - its ID comes from the effective backend passed to admission;
- both
RunandRunPrepareduse the shared boundary without duplicate wrapping; errors.Isstill reachescapacity.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.
- Add root
capacity_error.gowith the exact public type, methods, and GoDoc in Fixed Design. - Update
errors.goto translate a nonblank internal typed error into a fresh public value before general sentinel mapping. - Preserve the existing unstructured capacity fallback and all unrelated error-mapping order.
- Update
engine.goanddoc.gowith the exact operation, ownership, and unstable-JSON contracts. - Add the focused root translation test.
- Extend the existing external-package
Run, sentinel, cancellation, andRunPreparedcontract assertions described under Test Ownership. - 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:
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
RunandRunPreparedadmission rejection is discoverable as a public*CapacityErrorwith the selected backend ID; - the public error unwraps only to
ErrCapacityExceededand does not retain the internal typed error; - existing broad
errors.Ishandling 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.
- 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.
- Update the feature-roadmap, future-catalog, implementation-plan, and downstream-wishlist statuses and links exactly as specified above.
- Follow every added or changed Markdown link and confirm its file and heading target.
- Run the full validation sequence.
- Inspect the complete diff for scope, sensitive data, and repository hygiene.
- Only after every check passes, leave both roadmap statuses as
Completeand rerungit 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:
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.