Files
promptkit/docs/roadmap/implementation.md

521 lines
22 KiB
Markdown

# 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.