521 lines
22 KiB
Markdown
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.
|