diff --git a/docs/roadmap/capacity-errors.md b/docs/roadmap/capacity-errors.md new file mode 100644 index 0000000..a78f61f --- /dev/null +++ b/docs/roadmap/capacity-errors.md @@ -0,0 +1,262 @@ +# Structured Capacity Errors + +**Status:** Accepted. + +## Purpose + +Allow consumers to identify which registered backend rejected engine +admission without parsing an error string, while preserving the existing +`ErrCapacityExceeded` classification and Promptkit's provider-neutral error +boundary. + +This provides safe operational context requested by +[Notarius](notarius-promptkit-wishlist.md#priority-4-structured-capacity-errors) +and +[Weatherreporter](weatherreporter-promptkit-wishlist.md#structured-capacity-errors). + +## Motivation + +Promptkit currently returns an error matching `ErrCapacityExceeded` when a +limited backend's active and waiting admission capacity is full. That sentinel +lets consumers classify the failure reliably, but the selected backend ID is +available only in diagnostic text. + +Applications using multiple backends need the stable backend ID for +operator-facing diagnostics, metrics labels, failure receipts, and their own +retry decisions. Parsing `Error()` text would turn non-contractual wording +into an accidental API and risks retaining more diagnostic context than the +application needs. + +Promptkit already owns backend selection and the admission boundary. It should +attach the selected backend's safe identity at that boundary and translate it +into a small public typed error. + +## Consumer Workflow + +The target public workflow is: + +```go +result, err := engine.Run(ctx, request) +if err != nil { + var capacityErr *promptkit.CapacityError + if errors.As(err, &capacityErr) { + // Record capacityErr.BackendID or apply application retry policy. + } +} +``` + +Existing classification remains valid: + +```go +if errors.Is(err, promptkit.ErrCapacityExceeded) { + // Handle engine admission rejection without structured context. +} +``` + +The target public surface is: + +```go +type CapacityError struct { + BackendID string +} + +func (e *CapacityError) Error() string +func (e *CapacityError) Unwrap() error +``` + +`Unwrap` returns `ErrCapacityExceeded`. Engine-produced capacity errors +therefore support both `errors.As` to `*CapacityError` and +`errors.Is(err, ErrCapacityExceeded)`, including through additional wrapping. + +The exported declaration and GoDoc will own the exact implemented contract. +`CapacityError` does not need a stable JSON representation, an exported +constructor, or fields beyond `BackendID`. + +## Error Boundary + +A `CapacityError` represents one specific condition: `Run` or `RunPrepared` +was rejected at Promptkit's engine-local bounded admission boundary because +the selected limited backend had no remaining admitted slot. + +It does not represent: + +- waiting for an active-generation permit after admission; +- caller cancellation or deadline expiration while acquiring or holding + capacity; +- provider rate limiting, quota exhaustion, HTTP 429 responses, or another + model-client failure; +- validation, output repair, or application-level queue rejection; or +- an application's decision not to submit or retry work. + +Provider and injected-client errors retain their existing generation error +classification even when a provider describes its failure as capacity or rate +limiting. Promptkit must not infer an engine admission error from provider +status codes or error text. + +Admission continues to check caller cancellation before reporting a full +pool. A cancellation that wins that decision remains a context-related +execution error and does not become a `CapacityError`. + +## Backend Identity + +`BackendID` is the normalized stable ID of the registered backend whose pool +rejected admission. It is the same effective backend identity used for +capacity routing and exposed by prepared and run metadata. + +For ordinary `Run`, the ID comes from the backend selected by the resolved +prompt, profile, and execution target. For `RunPrepared`, it comes from the +backend identity frozen in the prepared execution. + +An endpoint override does not change backend identity and therefore does not +change the reported `BackendID`. Endpoint-only profiles and unlimited +backends have no bounded admission pool and cannot produce an engine-generated +`CapacityError`. + +Every engine-produced `CapacityError` has a nonblank registered backend ID. +The type does not report a profile ID, prompt ID, model, endpoint, run ID, or +prepared-handle identity. + +## Compatibility + +`ErrCapacityExceeded` remains the stable sentinel for broad classification. +This feature refines the error value returned by capacity-rejected `Run` and +`RunPrepared` calls without replacing or removing that sentinel. + +The following behaviors remain unchanged: + +- `errors.Is(err, ErrCapacityExceeded)` succeeds for engine admission + rejection; +- capacity rejection remains distinct from `ErrInvalidRequest`, + `ErrLLMGenerate`, and other public categories; +- `Run` returns no partial result after admission rejection; +- `RunPrepared` retains its existing one-attempt lifecycle, including that a + claimed handle is consumed when admission is rejected; +- admission ordering, limits, queue capacity, leases, release behavior, and + backend independence are unchanged; and +- `Prepare`, `PrepareExecution`, `InspectPrompt`, and `InspectProfile` perform + no admission and cannot return this error. + +Direct equality with the sentinel is not introduced as a contract. Consumers +continue to use `errors.Is` for category checks and `errors.As` for structured +context. + +## Sensitive Data And Diagnostics + +The typed error contains only `BackendID`. It must not expose or retain: + +- backend endpoints; +- credential environment-variable names or credential values; +- model, profile, prompt, session, or request identifiers; +- prompt, input, rendered, generated, or validation content; +- concurrency limits, queue capacities, current counts, or queue depth; +- request position or identities of other admitted work; or +- retry timing, provider health, or speculative availability. + +`Error()` may include the quoted backend ID and a concise admission-rejection +description. Its wording is diagnostic and not a stable parsing contract. +Default formatting, wrapping, and Go string formatting must not reveal +anything beyond the public field and generic capacity classification. + +Concurrency and queue configuration remain available where consumers define +their backends; repeating them on each error would add stale or unnecessary +operational detail. Current counts are race-prone observations rather than a +durable explanation of when capacity will become available. + +## Retry And Application Policy + +Promptkit reports that admission was rejected but does not retry, wait for a +new admission slot, calculate backoff, or declare an HTTP status. + +A full in-process admission pool does not provide a meaningful retry delay: +lease duration depends on consumer inputs, model latency, validation, repair, +cancellation, and provider behavior. Consumers own whether to retry, which +backend to use, how to schedule or back off, what to expose to operators, and +how to translate the error into CLI or transport behavior. + +The absence of retry metadata is deliberate. A future retry facility would +require its own roadmap and application-neutral policy rather than extending +this error speculatively. + +## Ownership And Concurrency + +Each rejected operation returns its own caller-owned `*CapacityError`. The +error does not reference mutable pool state or retain the request. Reading it +requires no engine access and remains safe after the rejected call returns. + +Mutating the exported `BackendID` on a returned error affects only that error +value. It cannot change the backend registry, capacity manager, another +operation's error, or later routing and admission. + +Constructing or returning the typed error requires no new process-global +state, cache, goroutine, or synchronization beyond existing capacity +management. + +## Architectural Boundaries + +The root `promptkit` package owns the exported error type, sentinel +compatibility, and public translation. The engine's internal capacity and +use-case components remain responsible for admission and for carrying the +selected backend identity to the facade. + +Internal error types or capacity-manager state must not cross the public +boundary. The public facade must obtain structured identity through an +internal typed or otherwise non-textual error contract; it must not parse the +runner's diagnostic string. + +The feature does not introduce a public capacity manager, backend registry, +queue, scheduler, metrics collector, or transport-specific error. It remains a +small refinement of the existing root error boundary. + +## Documentation + +When implemented, documentation should retain these ownership boundaries: + +- the exported declaration and GoDoc own the exact type, field, `Error`, + `Unwrap`, `errors.Is`, `errors.As`, and formatting contracts; +- the consumer guide demonstrates broad classification and optional backend + extraction without duplicating every error guarantee; +- internal capacity and runner documentation describe where backend identity + is attached and translated; +- the backend API continues to own capacity configuration; and +- the architecture and documentation policies continue to own library, + application, and documentation boundaries. + +## Non-Goals + +This work does not include: + +- changing concurrency limits, queue capacities, FIFO scheduling, or default + capacity policy; +- exposing configured limits, current counts, queue depth, or waiter + positions; +- reporting a retry delay, retryable boolean, HTTP status, CLI exit code, or + backoff policy; +- automatically retrying or rerouting rejected work; +- unifying provider rate limits or quota failures with engine admission; +- adding structured context to generation, validation, prompt, profile, or + other error categories; +- returning partial preparation or run results with the error; +- changing prepared-execution handle lifecycle; +- adding stable JSON for errors; +- exposing internal capacity or use-case types; or +- adding application logging, metrics emission, persistence, or failure + receipts. + +## Target End State + +After this work: + +- every engine admission rejection from `Run` or `RunPrepared` returns an + error discoverable as `*CapacityError`; +- the typed error contains the normalized selected backend ID and no other + structured field; +- existing `errors.Is(err, ErrCapacityExceeded)` handling continues to work; +- `errors.As` obtains backend identity without parsing diagnostic text; +- capacity rejection remains distinct from cancellation, model-client + failures, provider throttling, and application scheduling; +- endpoints, credentials, request content, capacity metrics, and speculative + retry information remain absent; +- retry, backoff, rerouting, metrics, persistence, and transport mapping remain + consumer responsibilities; and +- the existing capacity architecture and runtime behavior are otherwise + unchanged. diff --git a/docs/roadmap/future.md b/docs/roadmap/future.md index 8bf17ae..c3771ad 100644 --- a/docs/roadmap/future.md +++ b/docs/roadmap/future.md @@ -33,18 +33,9 @@ consumers. ## Ideas -### Structured capacity errors - -Add safe structured context to backend admission rejection, as requested by -[Notarius](notarius-promptkit-wishlist.md#priority-4-structured-capacity-errors) -and -[Weatherreporter](weatherreporter-promptkit-wishlist.md#structured-capacity-errors). - -- Preserve compatibility with `errors.Is(err, ErrCapacityExceeded)`. -- Support `errors.As` to obtain the stable backend ID. -- Do not expose endpoints, credential configuration or values, request - content, or speculative retry timing. -- Keep retry and backoff policy with downstream consumers. +Structured capacity errors have been selected for active planning in the +[focused feature roadmap](capacity-errors.md). No other ideas currently await +selection. ## Entry Format diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 06f818e..4e5ddfb 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,618 +1,439 @@ -# Prompt-Definition Inspection Implementation Plan +# Structured Capacity Errors Implementation Plan -**Status:** Complete. +**Status:** Ready for implementation. ## Purpose This document is the decision-complete implementation plan for -[prompt-definition inspection](prompt-inspection.md). It is written for a -coding agent that will implement each stage in order. +[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, consumer workflow, policy choices, -compatibility requirements, non-goals, and target end state. This document -owns the concrete design, file-level changes, implementation sequence, test -ownership, documentation work, validation commands, and completion gates. +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 the corresponding future-catalog and Weatherreporter - wishlist edits that may already be uncommitted. + 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 - `prompt-inspection.md`. -- Keep the supported API in the root `promptkit` package. Prompt repositories - and internal prompt definitions remain below Go's `internal/` boundary. -- Reuse the exact prompt source, ID/version selection, definition - normalization, referenced-content loading, and prompt hashing used by - `Prepare`, `PrepareExecution`, and `Run`. Do not create a second parser, - source abstraction, or hash algorithm. -- Preserve the observable validation order, error identities, and results of - existing preparation and execution methods. -- The new inspection operation must not require a profile, backend, request - override, credential, artifact, schema source, renderer, validator, - capacity admission, output repairer, or model client. -- Treat structural validation as the existing prompt-repository contract. - Do not add template parsing, input media-type enforcement, schema loading, - schema compilation, profile validation, source enumeration, or provider - connectivity checks. -- Return only the metadata accepted by the feature roadmap. Do not expose raw - YAML, prompt descriptions, source paths, message or session templates, - cache-control declarations, rendered content, or schema bodies. -- Return caller-owned public values. The input-definition slice must not alias - repository, runner, engine, or another result's state. -- Keep tests lean and behavior-focused. Reuse existing prompt-repository and - preparation coverage rather than duplicating their complete format and - source matrices. -- Update exact contracts in GoDoc with the exported declarations. Update - current-state consumer, format, and internal documentation only after the - corresponding code exists. + `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 -### Public API +### Internal Admission Error -Add these root-package values in `types.go` immediately after -`ProfileInspection`: +Add `internal/usecase/capacity_error.go` with this internal boundary type: ```go -type PromptInputDefinition struct { - Name string - Required bool - ContentType string - Description string +// CapacityError identifies bounded admission rejected for one selected +// backend. +type CapacityError struct { + BackendID string } -type PromptInspection struct { - PromptID string - PromptVersion string - PromptHash string - DefaultProfileID string - Inputs []PromptInputDefinition - OutputContract OutputContract +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 ``` -Add this method to `Engine` in `engine.go`, immediately before -`InspectProfile`: +The declaration and method GoDoc must establish: -```go -func (e *Engine) InspectPrompt( - ctx context.Context, - promptID string, - promptVersion string, -) (*PromptInspection, error) -``` +- 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. -Do not accept a `RunRequest`, inputs, variables, profile ID, output override, -or options on this method. Exact inspection of one prompt ID and optional -version is the complete operation. +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`. -`PromptInputDefinition` and `PromptInspection` have no stable JSON contract. -Do not add JSON tags, `MarshalJSON`, `UnmarshalJSON`, or custom string -representations. Their nested `OutputContract` continues to use its existing -stable JSON representation when encoded independently, but that does not make -the enclosing inspection value stable. +Do not add an exported constructor, custom formatter, `Is` method, JSON tags, +`MarshalJSON`, or `UnmarshalJSON`. Direct equality with +`ErrCapacityExceeded` is not a supported contract. -The exact type and method GoDoc must establish: +### Root Translation -- `promptID` is required and whitespace-only is invalid; -- nonblank `promptID` and `promptVersion` values are passed to ordinary prompt - selection unchanged rather than trimmed or otherwise canonicalized; -- lookup is case-sensitive and exact; -- an empty version succeeds only when exactly one definition has the selected - ID, while a nonempty version selects one exact ID/version pair; -- the engine's configured prompt source and existing source-option selection - are used without merging, fallback, or enumeration; -- a successful result proves that the selected definition and referenced - message content files were structurally loaded through the ordinary prompt - repository; -- input metadata is returned in definition order and includes name, required - status, content type, and description; -- `DefaultProfileID` is declared metadata only and is not resolved; -- `OutputContract` is the normalized declared contract, not a request-level - effective override; -- a JSON Schema path is returned when declared, but the schema and its - references are not loaded or compiled; -- `PromptHash` is the same opaque equality value used by - `PreparedRun.PromptHash` for the same selected definition and observed - source state; -- hash spelling, length, encoding, algorithm, and security properties are not - public contracts; -- prompt bodies, templates, source paths, schemas, rendered messages, and - execution settings are not returned; -- no profile, credential, artifact, rendering, schema, validation, capacity, - provider, or model-generation work is performed; -- the returned result and input slice are caller-owned; -- filesystem-backed inspection is a point-in-time lookup and does not freeze - a definition for later execution; -- a nil engine matches `ErrInvalidConfig`; -- a blank prompt ID matches `ErrInvalidRequest`; -- an absent exact ID or version matches `ErrPromptNotFound` and not - `ErrPromptLoad`; -- malformed, unreadable, duplicate, ambiguous, referenced-content, or hashing - failures match `ErrPromptLoad`; -- cancellation observed during lookup matches `ErrPromptLoad` while - preserving the context error through `errors.Is`; and -- the method returns no partial result on error. +Update `mapPublicError` in `errors.go` before its general +`publicErrorFor` mapping: -Update the `Engine` type GoDoc to include `InspectPrompt` among operations safe -for concurrent calls. Do not imply that mutable injected filesystems or other -collaborators become safe when their own contracts do not provide that -guarantee. +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. -Update `doc.go` in the same stage: +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. -- include `Engine.InspectPrompt` in the package operation list; -- include prompt inspection in the concurrency and caller-ownership summary; -- list `PromptInputDefinition` and `PromptInspection` among construction and - inspection values without stable JSON representations; and -- retain the existing statement that exposed hashes are opaque. +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 add either new value to the stable JSON list. +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. -### Internal Result +### Operation Contracts -Add this internal value to `internal/domain/domain.go` near -`PromptDefinition` and `PromptInput`: +Update the existing declarations and GoDoc without duplicating the full type +contract: -```go -// PromptInspection is the resolved result of exact prompt inspection. -type PromptInspection struct { - PromptID string - PromptVersion string - PromptHash string - DefaultProfileID string - Inputs []PromptInput - OutputContract OutputContract -} -``` +- 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 add JSON or YAML tags. The internal value is a use-case result, not a -source format or persistence contract. +Do not change method signatures or add the type to any stable JSON list. -The use-case operation must copy the definition's `Inputs` slice before -putting it on this result. `PromptInput` currently contains only scalar fields, -so a new slice with value copies is sufficient. +### Error And Identity Boundaries -### Shared Prompt Selection And Hashing +The implementation must preserve all of these distinctions: -Add `internal/usecase/prompt_inspection.go`. Define one private selection -value: +| 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 | -```go -type resolvedPromptDefinition struct { - definition *domain.PromptDefinition - hash string -} -``` - -Add a private runner helper: - -```go -func (r *Runner) resolvePromptDefinition( - ctx context.Context, - promptID string, - promptVersion string, -) (*resolvedPromptDefinition, error) -``` - -The helper must perform these operations in order: - -1. reject a blank or whitespace-only `promptID` with `ErrInvalidRequest`; -2. pass every nonblank ID and the version to the repository unchanged; -3. reject a nil runner or nil prompt repository with `ErrPromptLoad` rather - than panicking; -4. call `promptdef.Repository.GetPromptDefinition` exactly once; -5. wrap every repository error as `ErrPromptLoad` with `%w` while preserving - the repository error, including `promptdef.ErrPromptDefinitionNotFound`; -6. reject a nil definition returned without an error as `ErrPromptLoad`; -7. calculate the equality value with the existing - `hashPromptDefinition` function exactly once; -8. classify a hash failure as `ErrPromptLoad` using the existing preparation - wording and behavior; and -9. return the repository-owned definition for internal per-call use and the - hash. - -Do not trim a nonblank ID or version, clone the complete prompt definition, -parse its templates, inspect multiple definitions, load schemas, or resolve -the default profile. - -Refactor the prompt-loading and hashing block in -`Runner.resolvePreparation` to call `resolvePromptDefinition`. Retain the -existing early blank-ID check before direct session normalization so a request -with both a blank prompt ID and an invalid direct session preserves its -current error priority. The shared helper may defensively repeat the blank-ID -check. - -After the helper returns, ordinary preparation uses -`selection.definition` and `selection.hash` exactly where it currently uses -the repository result and prompt hash. Leave direct-session normalization, -profile selection, target resolution, credentials, output-contract -resolution, artifacts, schemas, rendering, admission, generation, and all -later ordering unchanged. - -This refactor must preserve: - -- current prompt ID and version selection behavior; -- existing public not-found versus prompt-load classification; -- prompt default-profile selection; -- prompt hashing before any request-specific definition copy or session - template clearing; -- `PreparedRun.PromptHash`, `RunResult.PromptHash`, and prepared-execution - behavior; and -- all current `Prepare`, `PrepareExecution`, `Run`, and `RunPrepared` results - and failure ordering. - -Do not change `hashPromptDefinition` or introduce a second equality -calculation. Its current serialization and SHA-256 implementation remain an -internal mechanism behind the opaque public value. - -### Internal Inspection Operation - -Add this method in `internal/usecase/prompt_inspection.go`: - -```go -func (r *Runner) InspectPrompt( - ctx context.Context, - promptID string, - promptVersion string, -) (*domain.PromptInspection, error) -``` - -It performs these operations: - -1. reject a blank or whitespace-only ID as `ErrInvalidRequest`; -2. if the supplied context is already canceled, return an error wrapping both - `ErrPromptLoad` and `ctx.Err()` without consulting the repository; -3. call `resolvePromptDefinition` with the original ID and version; -4. allocate and copy the selected definition's input slice in its existing - order; -5. return the selected definition's normalized ID, version, default profile, - declared `Validation` contract, copied inputs, and shared prompt hash. - -The returned `OutputContract` comes directly from the loaded definition's -normalized `Validation` value. Do not call `resolveOutputContract`, because no -request override participates in inspection. - -Do not call profile or backend repositories, `resolveProfileSelection`, -`resolveExecutionTarget`, credential checks, schema loaders, artifact readers, -renderers, validators, capacity admission, output repair, or the model client. -Do not return the definition itself. - -The operation returns no partial result. Repository cancellation that occurs -after lookup begins remains wrapped by `ErrPromptLoad` through the shared -helper while preserving the underlying context error where the repository -does so. - -### Root Conversion - -Add this conversion in `convert.go` near -`fromDomainProfileInspection`: - -```go -func fromDomainPromptInspection( - inspection *domain.PromptInspection, -) *PromptInspection -``` - -The conversion must: - -- return `nil` for a nil internal value; -- copy every scalar identity, hash, and default-profile field; -- allocate a new `[]PromptInputDefinition` in the same order and copy every - name, required flag, content type, and description; -- convert the contract through the existing - `fromDomainOutputContract`; and -- return a non-aliased root value. - -Do not expose internal `PromptInput` or `PromptDefinition` types at the root -boundary. Do not use serialization as a copying mechanism. - -### Root Facade And Error Mapping - -`Engine.InspectPrompt` follows the existing inspection facade pattern: - -1. reject a nil engine or nil runner with `ErrInvalidConfig`; -2. pass the context, ID, and version directly to - `Runner.InspectPrompt`; -3. map internal failures through the existing `mapPublicError`; and -4. convert a successful result with `fromDomainPromptInspection`. - -No request conversion is needed. Do not add a public sentinel or typed error. - -The existing error mapping already has the required ordering: - -- an underlying `promptdef.ErrPromptDefinitionNotFound` maps to - `ErrPromptNotFound` before the enclosing use-case `ErrPromptLoad` is - considered; -- other `usecase.ErrPromptLoad` failures map to `ErrPromptLoad`; and -- `usecase.ErrInvalidRequest` maps to `ErrInvalidRequest`. - -Do not reorder or otherwise change `errors.go` unless a focused public test -proves the current mapping fails a required identity. Preserve underlying -repository and context errors through `errors.Is`. - -### Ownership, Consistency, And Concurrency - -The internal result has a copied input slice, and the root conversion creates -another public slice. A consumer may mutate the result and its inputs without -affecting: - -- the prompt repository or selected definition; -- the engine; -- a later `InspectPrompt` call; -- `Prepare`, prepared execution, or `Run`; or -- another result already returned to a caller. - -`OutputContract` and each input element contain scalar values, so no deeper -mutable tree exists in the accepted result shape. - -No new mutable engine state, source cache, global registry, lock, or goroutine -is needed. Concurrency safety follows from existing repository contracts and -per-call result allocation. Inspection does not freeze mutable source state; -prepared execution remains the exact snapshot-to-execution workflow. +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 -Add focused internal tests in -`internal/usecase/prompt_inspection_test.go`. Reuse package fakes where doing -so remains clearer than adding a new fixture, or define one small counting -prompt repository local to this test file. +Extend existing tests at their current ownership boundaries. -The internal tests own: +`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. -- blank-ID rejection without repository access; -- a pre-canceled context matching both `ErrPromptLoad` and the context error - without repository access; -- exactly one repository lookup with the original nonblank ID and version; -- a successful result containing normalized identity, hash, default profile, - complete ordered input metadata, and the declared output contract; -- successful operation with every non-prompt runner collaborator nil; -- copied inputs across repeated inspections; -- missing-prompt wrapping that retains - `promptdef.ErrPromptDefinitionNotFound`; -- nil repository and nil returned definition defenses; and -- representative preservation of ordinary preparation after it is switched - to the shared selection-and-hash helper. +`internal/usecase/runner_test.go` owns attachment of selected identity. +Update `TestRunnerAdmissionFailureSkipsCompletionCollaborators` so: -Combine these into a small number of readable behavioral tests. Prompt -repository tests remain the owners of YAML discovery, strict decoding, -normalization, content-file containment and reading, duplicate selection, and -ID/version matrices. Existing preparation tests remain the owners of session, -profile, artifact, schema, rendering, credential, and execution ordering. +- 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. -Add external-package public contract tests in `public_contract_test.go`. They -own: +Replace the current string-content assertion with the structured assertion. +Do not test exact diagnostic wording. -- the exported method and result shape through normal Go use; -- successful exact-version inspection from an `fs.FS` prompt source with - multiple versions; -- normalized input and output metadata, including a declared JSON Schema path, - without configuring or loading a schema source; -- referenced `content_file` loading without returning its body; -- no default-profile resolution, demonstrated by a nonexistent declared - profile; -- no model invocation, using an existing deterministic fake client where - useful; -- nil-engine, blank-ID, missing exact version, ambiguous omitted version, - malformed definition or referenced-content, and pre-canceled public error - identities; -- that not-found does not also match `ErrPromptLoad`; -- caller ownership across repeated inspection and later preparation; and -- equality between `PromptInspection.PromptHash` and - `PreparedRun.PromptHash` for the same prompt and observed source state. +`errors_internal_test.go` owns root translation. Add a focused test that maps +an internal `*usecase.CapacityError` and proves: -Use a simple executable prompt with an in-memory profile for the hash -equivalence test. Use a separate JSON Schema declaration for the -schema-independent inspection test so `Prepare` is not needed there. +- 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. -Do not add JSON golden or round-trip tests because the new inspection values -deliberately have no stable JSON contract. Do not duplicate the full -prompt-repository parser, source-option precedence, schema-validation, -preparation, or public error suites at the root layer. +Retain the existing cancellation-preservation test. -Existing tests that must continue passing without weakened assertions include: +`capacity_contract_test.go` owns assembled public `Run` behavior. Extend +`TestEngineRejectsRunBeforeCompletionWhenAdmissionIsFull` to prove: -- prompt-definition filesystem and `fs.FS` repository tests; -- prompt-source option replacement tests; -- `Prepare` and `Run` prompt selection and hashing tests; -- direct-session prompt-hash invariance tests; -- prepared-execution frozen-source and details tests; -- public error mapping tests; and -- profile inspection tests. +- 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 implementation and public tests pass, update current-state -documentation: +After the code and contract tests pass: -- `docs/consumers/pkg-promptkit.md`: add a concise task-oriented section after - engine construction showing `Engine.InspectPrompt`, explaining how a - consumer can validate declared inputs and output workflow before preparing, - and distinguishing it from `InspectProfile`, `Prepare`, and prepared - execution. Link to GoDoc for exact fields and errors. -- `docs/formats.md`: note that exact prompt inspection uses the same prompt - source, strict decoding, content-file resolution, and ID/version selection - described by the format reference. Do not redefine the exported method. -- `docs/internal/runner.md`: add a shared prompt-selection boundary for - inspection and preparation, including shared hashing, and explain that - inspection stops before every execution-dependent collaborator. -- `docs/internal/sources.md`: record that exact prompt inspection performs one - point-in-time prompt lookup, validates referenced message content through - the repository, and does not parse templates or read profile, input, or - schema sources. -- `docs/internal/overview.md`: add prompt inspection to the existing root - facade and `internal/usecase` responsibility descriptions. Do not add a new - component or package row. -- `docs/roadmap/future.md`: remove the statement that prompt inspection is in - active planning and leave structured capacity errors intact. -- `docs/roadmap/weatherreporter-promptkit-wishlist.md`: change the prompt - inspection disposition from accepted planning to implemented behavior, - link to durable consumer guidance or GoDoc, and remove or condense proposed - API detail that would compete with the implemented declarations. -- `docs/roadmap/prompt-inspection.md`: change its status to `Complete` only - after code, tests, current-state documentation, and full validation are - complete. -- `docs/roadmap/implementation.md`: change its status to `Complete` only after - every completion gate in this plan is satisfied. +- 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. -The accepted feature roadmap already describes only purpose, scope, policy, -and target end state; it contains no stage sequence to remove during this -planning pass. +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. -Do not update release guidance in this feature implementation. A later release -pass decides whether the additive API warrants a supplemental release -document. +When implementation is complete: -## Stage 1: Share Prompt Selection And Implement Internal Inspection +- 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 -Add the internal inspection result and operation, share exact prompt loading -and hashing with ordinary preparation, and prove the internal behavior without -publishing the root API yet. +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`. Read the complete feature roadmap, -implementation rules, and fixed design above before editing. +`docs/roadmap/implementation.md`. -1. Add `domain.PromptInspection` to `internal/domain/domain.go` without source - or serialization tags. -2. Add `internal/usecase/prompt_inspection.go` with - `resolvedPromptDefinition`, `resolvePromptDefinition`, and - `Runner.InspectPrompt` exactly as specified. -3. Refactor `Runner.resolvePreparation` in `internal/usecase/runner.go` to use - the shared prompt selection and hash while preserving existing validation - and execution ordering. -4. Add lean behavioral tests in - `internal/usecase/prompt_inspection_test.go`. -5. Run the focused validation below and repair regressions before ending the - stage. +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 add the root public type or method, update current-state documentation, -or alter prompt formats, template behavior, profile selection, schema -handling, capacity, validation, model-client, or provider behavior in this -stage. +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: +Run from the repository root: ```sh -gofmt -w internal/domain/domain.go \ - internal/usecase/prompt_inspection.go \ - internal/usecase/prompt_inspection_test.go \ - internal/usecase/runner.go -go test ./internal/usecase ./internal/promptdef -go test ./internal/usecase -run \ - 'TestRunner(InspectPrompt|Prepare|Run|PrepareExecution|RunPrepared)' -go vet ./internal/usecase ./internal/promptdef +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 ``` -If actual existing test names do not match the focused expression, run the -smallest truthful package or expression that covers the listed behavior -rather than weakening or skipping assertions. - ### Completion Gate Stage 1 is complete only when: -- inspection loads and hashes one exact prompt through the configured - repository; -- prompt selection and hashing are shared with ordinary preparation; -- nonblank ID and version values reach the repository unchanged; -- successful inspection needs no non-prompt runner collaborator; -- inputs are copied and no definition or content body escapes; -- cancellation and load failures preserve the required internal identities; -- ordinary preparation and execution behavior remains unchanged; and -- no root public API or current-state documentation claims the feature yet. +- 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: Publish The Root Facade And Public Contract +## Stage 2: Expose And Protect The Public Error Contract ### Objective -Expose the minimal caller-owned inspection API through `Engine`, preserve -public error and JSON compatibility, and protect the consumer-visible -contract. +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. -Re-read the fixed public API, conversion, error, ownership, and test sections -before editing. -1. Add `PromptInputDefinition` and `PromptInspection` with exact GoDoc to - `types.go` immediately after `ProfileInspection`. Do not add JSON tags. -2. Add `fromDomainPromptInspection` to `convert.go` using a newly allocated - input slice and the existing output-contract converter. -3. Add `Engine.InspectPrompt` and exact GoDoc to `engine.go` immediately before - `InspectProfile`. -4. Update `Engine` GoDoc to include concurrent prompt inspection. -5. Update package GoDoc in `doc.go` for operation discovery, concurrency, - ownership, opaque hashes, and non-stable JSON classification. -6. Add compact external-package contract coverage in - `public_contract_test.go`, reusing existing fixtures and fakes where they - remain clear. -7. Confirm that existing `errors.go` mapping meets the plan; do not change it - unless a required public identity test fails for a genuine mapping reason. -8. Run the focused validation below and repair regressions before ending the - stage. +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 add enumeration, source validation across a corpus, request overrides, -template or schema output, profile resolution, JSON stability, caching, new -errors, or current-state prose documentation in this stage. +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: +Run from the repository root: ```sh -gofmt -w doc.go types.go convert.go engine.go public_contract_test.go -go test . -go test ./internal/usecase ./internal/promptdef +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 \ - 'Test(InspectPrompt|.*Prompt.*PublicError|.*Prompt.*Contract)' -go vet . -go build . + 'TestMapPublicError|TestEngineRejectsRunBeforeCompletionWhenAdmissionIsFull|TestCapacityExceededSentinelContract|TestPreparedExecutionCredentialCapacityAndTimingBoundaries' +go test . +git diff --check ``` -If the repository's actual focused test names differ, use the implemented test -names rather than weakening or skipping the intended assertions. - ### Completion Gate Stage 2 is complete only when: -- a consumer can call `Engine.InspectPrompt` through the root package; -- exact ID/version selection matches ordinary preparation; -- the result contains only accepted identity, input, default-profile, - output-contract, and opaque equality metadata; -- no prompt body, schema body, profile, credential, or execution setting is - exposed or resolved; -- nil, blank, missing, malformed, ambiguous, referenced-content, and - cancellation errors have the required public identities; -- not-found remains distinct from prompt-load failure; -- `PromptHash` matches ordinary preparation for the same source state; -- repeated calls and caller mutation cannot alter engine-owned state; -- no stable JSON or new error contract was introduced; and -- existing public preparation, execution, profile-inspection, and stable JSON - tests remain unchanged and passing. +- 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 Documentation And Validate The Repository +## Stage 3: Update Canonical Documentation And Validate ### Objective -Make implemented prompt inspection discoverable in its canonical -documentation, reconcile temporary roadmap state, and complete full repository -validation. +Make the implemented structured error discoverable, reconcile temporary +planning state, and complete repository-wide validation. ### Implementation Prompt @@ -620,32 +441,34 @@ Implement only Stage 3 of `docs/roadmap/implementation.md` after Stages 1 and 2 satisfy their completion gates. -1. Update `docs/consumers/pkg-promptkit.md`, `docs/formats.md`, - `docs/internal/runner.md`, `docs/internal/sources.md`, and - `docs/internal/overview.md` according to the documentation ownership - section above. -2. Update the future catalog and Weatherreporter wishlist disposition so they - no longer describe prompt inspection as merely accepted work. -3. Check every changed Markdown link and confirm its file and heading target. -4. Run the full validation sequence below. -5. Only after every check passes, set the feature roadmap and this - implementation plan to `**Status:** Complete.` -6. Re-run `git diff --check` after the status edits. +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 release notes, a new example program, a new public package, or a -duplicate API reference. Keep detailed contracts in GoDoc and task-oriented -usage in the consumer guide. +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/domain/domain.go \ - internal/usecase/prompt_inspection.go \ - internal/usecase/prompt_inspection_test.go \ +gofmt -w internal/usecase/capacity_error.go \ internal/usecase/runner.go \ - doc.go types.go convert.go engine.go public_contract_test.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 ./... @@ -657,32 +480,35 @@ git status --short ``` The `gofmt -l` command must print no paths. The maintained example must remain -offline and must not require a real credential or provider. +offline and require no real credential or provider. -Inspect the final diff and confirm: +Inspect the final state and confirm: - only files required by this feature and pre-existing user changes are present; -- no credentials, private source content, workspace files, local - replacements, generated binaries, or unrelated formatting changes were - added; -- the public declarations and GoDoc own exact API behavior; -- current-state documentation describes only implemented behavior and links - to canonical owners; -- roadmap documents contain scope or completion status rather than a - duplicate current API reference; and +- 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 -The implementation is complete only when: +Implementation is complete only when: - every Stage 1 and Stage 2 gate remains satisfied; -- the complete ordinary and race-enabled suites pass; -- vet, build, formatting, the maintained offline example, Markdown links, and - whitespace checks pass; -- consumer, format, internal, future, and wishlist documentation are - consistent with the implemented boundary; +- 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 @@ -690,5 +516,5 @@ The implementation is complete only when: ## Open Questions -None. The accepted feature roadmap and the fixed decisions above fully specify -the implementation boundary. +None. The accepted feature roadmap and fixed design above fully specify the +implementation boundary. diff --git a/docs/roadmap/notarius-promptkit-wishlist.md b/docs/roadmap/notarius-promptkit-wishlist.md index e38e2cb..a0e89e3 100644 --- a/docs/roadmap/notarius-promptkit-wishlist.md +++ b/docs/roadmap/notarius-promptkit-wishlist.md @@ -206,7 +206,7 @@ resolution. ## Priority 4: Structured Capacity Errors **Disposition:** Accepted into the -[future catalog](future.md#structured-capacity-errors). +[structured capacity errors](capacity-errors.md) feature roadmap. ### Downstream need diff --git a/docs/roadmap/weatherreporter-promptkit-wishlist.md b/docs/roadmap/weatherreporter-promptkit-wishlist.md index 189e6e4..1bb0789 100644 --- a/docs/roadmap/weatherreporter-promptkit-wishlist.md +++ b/docs/roadmap/weatherreporter-promptkit-wishlist.md @@ -283,7 +283,7 @@ the existing sentinel remain available. ### Structured Capacity Errors **Disposition:** Accepted into the -[future catalog](future.md#structured-capacity-errors). +[structured capacity errors](capacity-errors.md) feature roadmap. The typed capacity error proposed by the [Notarius wishlist](notarius-promptkit-wishlist.md#priority-4-structured-capacity-errors) diff --git a/internal/capacity/manager_test.go b/internal/capacity/manager_test.go index 97d729b..cb44750 100644 --- a/internal/capacity/manager_test.go +++ b/internal/capacity/manager_test.go @@ -124,6 +124,11 @@ func TestManagerAdmissionHonorsContextAndUnlimitedBackends(t *testing.T) { if err != nil { t.Fatalf("construct manager: %v", err) } + release, err := manager.Admit(context.Background(), "limited") + if err != nil { + t.Fatalf("fill limited pool: %v", err) + } + defer release() ctx, cancel := context.WithCancel(context.Background()) cancel() diff --git a/internal/usecase/capacity_error.go b/internal/usecase/capacity_error.go new file mode 100644 index 0000000..2c89528 --- /dev/null +++ b/internal/usecase/capacity_error.go @@ -0,0 +1,24 @@ +package usecase + +import ( + "fmt" + "strings" + + "gitea.maximumdirect.net/eric/promptkit/internal/capacity" +) + +// CapacityError identifies bounded admission rejected for one selected backend. +type CapacityError struct { + BackendID string +} + +func (e *CapacityError) Error() string { + if e == nil || strings.TrimSpace(e.BackendID) == "" { + return capacity.ErrCapacityExceeded.Error() + } + return fmt.Sprintf("backend %q admission: %v", e.BackendID, capacity.ErrCapacityExceeded) +} + +func (e *CapacityError) Unwrap() error { + return capacity.ErrCapacityExceeded +} diff --git a/internal/usecase/runner.go b/internal/usecase/runner.go index 08d6c03..7ef35c2 100644 --- a/internal/usecase/runner.go +++ b/internal/usecase/runner.go @@ -390,8 +390,8 @@ func (r *Runner) admitRun(ctx context.Context, backendID string) (func(), error) } release, err := r.admitter.Admit(ctx, backendID) if err != nil { - if errors.Is(err, capacity.ErrCapacityExceeded) { - return nil, fmt.Errorf("backend %q admission: %w", backendID, err) + if errors.Is(err, capacity.ErrCapacityExceeded) && strings.TrimSpace(backendID) != "" { + return nil, &CapacityError{BackendID: backendID} } return nil, err } diff --git a/internal/usecase/runner_test.go b/internal/usecase/runner_test.go index c8cab0b..7a62a58 100644 --- a/internal/usecase/runner_test.go +++ b/internal/usecase/runner_test.go @@ -1357,14 +1357,14 @@ func TestRunnerAdmissionUsesResolvedBackendIdentity(t *testing.T) { func TestRunnerAdmissionFailureSkipsCompletionCollaborators(t *testing.T) { tests := []struct { - name string - admissionError error - wantBackendContext bool + name string + admissionError error + wantCapacityType bool }{ { - name: "capacity exhausted", - admissionError: capacity.ErrCapacityExceeded, - wantBackendContext: true, + name: "capacity exhausted", + admissionError: capacity.ErrCapacityExceeded, + wantCapacityType: true, }, { name: "context canceled", @@ -1414,8 +1414,16 @@ func TestRunnerAdmissionFailureSkipsCompletionCollaborators(t *testing.T) { if errors.Is(err, ErrInvalidRequest) || errors.Is(err, ErrLLMGenerate) { t.Fatalf("admission error was recategorized: %v", err) } - if tc.wantBackendContext && !strings.Contains(err.Error(), "custom") { - t.Fatalf("capacity error lacks backend context: %v", err) + var capacityErr *CapacityError + if tc.wantCapacityType { + if !errors.As(err, &capacityErr) { + t.Fatalf("capacity error=%v, want internal typed identity", err) + } + if capacityErr.BackendID != "custom" { + t.Fatalf("capacity backend ID=%q, want custom", capacityErr.BackendID) + } + } else if errors.As(err, &capacityErr) { + t.Fatalf("non-capacity admission error exposed typed capacity identity: %v", err) } if !reflect.DeepEqual(admitter.backendIDs, []string{"custom"}) { t.Fatalf("admitted backend IDs=%#v, want custom", admitter.backendIDs)