Files
promptkit/docs/roadmap/implementation.md

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 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:

// 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:

// 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:

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:

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:

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.