From 8745d256bd5543b3b8cf05040a885dd77f532743 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 26 Aug 2026 01:53:22 +0000 Subject: [PATCH] Plan appended request messages --- docs/roadmap/appended-request-messages.md | 434 +++++++++++++++++++++ docs/roadmap/implementation.md | 436 ++++++++++++++++++++++ 2 files changed, 870 insertions(+) create mode 100644 docs/roadmap/appended-request-messages.md create mode 100644 docs/roadmap/implementation.md diff --git a/docs/roadmap/appended-request-messages.md b/docs/roadmap/appended-request-messages.md new file mode 100644 index 0000000..f04343d --- /dev/null +++ b/docs/roadmap/appended-request-messages.md @@ -0,0 +1,434 @@ +# Appended Request Messages + +## Status + +Accepted for future implementation. This roadmap records a concrete downstream +requirement from Notarius and the Promptkit-native design selected for it. It +describes future behavior only; the current public API and canonical +documentation remain authoritative until the feature is implemented. + +## Purpose + +Allow a Go consumer to execute an ordinary configured prompt with an explicit +sequence of already-rendered chat messages appended after the +prompt-definition messages. The capability should preserve Promptkit's normal +preparation, inspection, hashing, validation, structured-output repair, +capacity, cancellation, credential, error, and debug boundaries. + +The immediate consumer is Notarius, which needs to resubmit the same configured +prompt after semantic validation rejects an LLM-produced stage candidate, then +append: + +1. an `assistant` message containing the exact defective response; and +2. a `user` message containing bounded application-owned feedback and a request + for one corrected replacement response. + +Promptkit should provide the safe message-composition mechanism. Notarius +should continue to own semantic validation, stage retry budgets, correction +wording, terminal policy, and the decision to start another completion. + +## Consumer Need + +Promptkit v0.8 can perform bounded structural repair inside one `Run` or +`RunPrepared` operation. Its repairer correctly starts from the complete +original rendered prompt, appends the latest candidate as an assistant turn, +and adds a corrective user turn. That loop is intentionally driven by +Promptkit's own structural validators. + +Notarius has a distinct outer workflow: + +1. Promptkit returns a structurally valid response. +2. Notarius materializes a typed stage candidate. +3. One or more application validators evaluate domain semantics. +4. If the candidate is rejected and a Notarius stage attempt remains, Notarius + makes a new Promptkit request with application-owned corrective turns + appended after the ordinary configured prompt. + +Each `Run`, `Prepare`, or `PrepareExecution` remains an independent operation +that resolves current sources and inputs. Promptkit does not retain or reuse a +base prompt across calls, so the ordinary prefix is identical across attempts +only when the selected definitions, inputs, variables, and other preparation +inputs remain identical. A consumer that requires a pre-execution equality +check can prepare each attempt and compare its opaque rendered-prompt hash +before calling `RunPrepared`. A reusable prepared conversation or frozen +cross-run prompt is outside this feature. + +Today a `RunRequest` can select and parameterize a prompt definition, but it +cannot append downstream-owned messages. Registering a second prompt manifest +for every correction-capable operation would duplicate prompt identity, +profile defaults, input declarations, output contracts, and message assets. +Calling an injected model client directly would bypass Promptkit preparation, +capacity, structured output, repair, error adaptation, and provenance. Neither +workaround is a suitable long-term consumer contract. + +## Target Public API + +Add an append-only field to `RunRequest`: + +```go +type RunRequest struct { + // Existing fields omitted. + + AppendedMessages []RenderedMessage +} +``` + +Retain `RenderedMessage.Role` as a string for source compatibility and expose +the untyped string constants `RoleDeveloper`, `RoleSystem`, `RoleUser`, and +`RoleAssistant`. Their values are `developer`, `system`, `user`, and +`assistant`, respectively. The supported values and their semantics are part +of the public contract. + +`AppendedMessages` contains fully rendered message values. Promptkit normalizes +and validates their roles as described below, but does not interpret their +content as Go templates, resolve input references inside it, or load it from +files. Nil and empty slices are equivalent. + +Reusing `RenderedMessage` is preferred because it is already Promptkit's +public, stable representation of a provider-bound chat message and includes +the supported cache-control metadata. Its GoDoc should be generalized from a +definition-supplied message to an already-rendered chat message. A new +request-only message type would duplicate the same role, content, and cache +control contract without adding useful type safety. + +The exact exported declarations and semantics, once implemented, belong in +GoDoc. The name `AppendedMessages` is intentional: placement is part of the +contract, whereas names such as `AdditionalMessages` leave ordering +ambiguous. + +## Composition Semantics + +For `Prepare`, `PrepareExecution`, and `Run`, Promptkit should: + +1. perform ordinary prompt selection, input loading, variable processing, and + prompt rendering; +2. preserve every ordinary rendered message after its role has passed the + shared canonical normalization, including content, cache-control value, + order, and bytes, without rerendering or rewriting content; +3. normalize and validate request-supplied roles, then defensively copy and + append `RunRequest.AppendedMessages` in caller-supplied order; and +4. treat the combined sequence as the effective rendered prompt for every + later preparation and execution boundary. + +The request-supplied messages always follow all prompt-definition messages. +They cannot prepend, insert, replace, or delete definition messages. This +narrow contract protects a stable prompt prefix for provider-side prompt +caching and keeps prompt definitions as the canonical owner of ordinary +prompt content. + +Promptkit must not synthesize separators or normalize message content. Content, +including empty or whitespace-only content, remains byte-for-byte caller data; +whether a provider accepts it is provider policy. The effective session ID, +selected profile, model settings, output contract, structured-output +constraint, and input hashes retain their ordinary resolution rules. + +`InspectPrompt` remains definition inspection and therefore does not accept or +report request-supplied appended messages. + +## Preparation, Hashing, And Inspection + +Appended messages must participate in the same immutable preparation boundary +as ordinary rendered messages: + +- `PreparedRun.Messages` contains the complete combined sequence; +- `PreparedExecution.Details().Messages` contains an independent defensive + copy of that sequence; +- `Run` and `RunPrepared` send exactly that sequence to the selected model + client; +- `RenderedPromptHash` covers the effective session ID and complete combined + sequence, including appended-message cache-control metadata; +- `PromptHash` continues to identify only the selected prompt definition and + therefore does not change merely because appended messages differ; and +- input hashes continue to describe only named artifacts. + +Two requests with identical prompt selection, inputs, variables, session, and +appended messages should prepare equivalent message sequences and rendered +prompt hashes. Changing any normalized appended role, content, order, or +cache-control value should change the rendered prompt hash. The hash encoding +must frame sessions, messages, and fields unambiguously rather than relying on +separators that could also occur in caller-controlled content. + +Prepared execution must freeze appended messages at preparation time. Caller +mutation after `PrepareExecution` returns must not affect its details or later +execution, and mutation of a value returned by `Details` must not affect the +opaque executable snapshot. + +## Interaction With Structured-Output Repair + +The combined message sequence is the complete original prompt for one +Promptkit operation. If Promptkit's structural validator rejects a generated +candidate and a repair attempt is available, the existing repair workflow +should begin from a fresh copy of that combined sequence and then append its +own candidate and correction messages. + +For the Notarius use case, the ordering may therefore be: + +1. ordinary configured prompt messages; +2. Notarius's previous assistant response; +3. Notarius's semantic-correction user message; +4. Promptkit's latest structurally defective assistant response, if any; and +5. Promptkit's structural-repair user message, if needed. + +This is not a recursive retry policy. Promptkit still owns only its configured +structural-repair budget within the current operation. It does not interpret +the purpose of consumer-supplied messages, invoke application validators, or +start a later Notarius stage attempt. + +All initial and repair generation calls must continue through the ordinary +prepared-execution, capacity, client, cancellation, credential, structured +output, usage, and error-adaptation paths. + +## Message Validation And Provider Boundaries + +Promptkit should validate appended messages before publishing a prepared value +or starting model generation. Validation should be shared by `Prepare`, +`PrepareExecution`, and `Run` and should reject: + +- invalid UTF-8 in role or content; +- a role that, after normalization, is not `developer`, `system`, `user`, or + `assistant`; or +- invalid cache-control metadata. + +Role normalization is intentionally limited to trimming surrounding whitespace +and converting the result to lowercase. Promptkit performs no aliasing or +semantic role translation. In particular, it must not translate `developer` +to `system` or the reverse. `system` is generally the more portable choice +across heterogeneous OpenAI-compatible backends; `developer` remains supported +for providers and models that implement the newer instruction hierarchy. A +backend or model that rejects an otherwise supported role reports that through +the ordinary provider error path. + +The same four-role vocabulary and normalization apply to prompt-definition +messages. This deliberately tightens the existing prompt format, which +currently accepts any nonblank role. `tool` is not supported because +Promptkit's text-only message value cannot express its required `tool_call_id`; +the deprecated `function` role likewise requires a name that the value cannot +represent. Those roles should be considered only as part of a deliberate +future tool-call message design. + +Promptkit does not impose appended-message count, per-message size, aggregate +size, estimated-token, or context-window limits, and it must not truncate +caller content. Those limits are backend-, model-, and application-specific. +Provider rejection for context length, role support, empty content, or any +other provider rule should cross the existing generation-error boundary with +its supported status and provider-detail information intact. + +Invalid appended messages are invalid requests and should preserve the public +`ErrInvalidRequest` identity. Error text should identify the failing appended +message index and violated property without echoing its role or content. An +invalid role in a prompt definition remains a source-owned prompt-definition +failure through its ordinary public error category. + +## Ownership, Privacy, And Formatting + +`RunRequest.AppendedMessages` is caller-owned. Promptkit must defensively copy +the slice and nested cache-control values before retaining them. Internal +domain conversion, prepared-run cloning, public result conversion, and +prepared-execution details must preserve the same non-aliasing guarantees as +ordinary rendered messages. + +Appended content can contain model output, source excerpts, or application +validation feedback and must be treated as sensitive prompt content: + +- `RunRequest.String` and `RunRequest.GoString` should report only an appended + message count, not roles or content; +- errors must not reproduce appended content; +- no credential or secret value may be inferred, copied, or added to hashes + beyond the existing prompt-content behavior; +- prepared values continue to expose complete rendered messages by design and + remain subject to the consumer's data-handling policy; and +- Promptkit should not add logging, persistence, or a durable retry transcript + for appended messages. + +The field does not need a stable `RunRequest` JSON contract because +`RunRequest` has none today. The stable JSON representation of prepared values +must continue to include the final `Messages` sequence through its existing +contract. + +## Application-Neutral Boundary + +This feature is a generic prompt-composition primitive. Promptkit should not +add concepts such as semantic validator, rejection, stage, pipeline, +correction reason code, or terminal validation policy to its public API. + +The following remain downstream responsibilities: + +- deciding whether a new request is warranted; +- choosing and enforcing the outer retry budget; +- retaining the exact earlier candidate; +- constructing, bounding, and redacting application feedback; +- determining which message roles and sequence express that workflow; +- keeping preparation inputs stable when an identical prefix is required, or + checking prepared hashes before execution; +- aggregating semantic validation findings; +- recording application provenance; and +- deciding whether exhausted retries fail, reject, warn, or continue. + +Promptkit owns only safe composition and execution of the effective request. + +## Documentation End State + +In the target state, the capability is described by the following canonical +owners: + +- root GoDoc for `RunRequest`, `RenderedMessage`, `PreparedRun`, and relevant + engine operations, including the role constants and normalization contract; +- the [framework format reference](../formats.md) for the tightened role + vocabulary and normalization shared by prompt-definition messages; +- the [Go consumer guide](../consumers/pkg-promptkit.md) with one concise + appended-message example and a warning that values are already rendered and + potentially sensitive; +- the [internal runner document](../internal/runner.md) for validation, + composition, hashing, preparation, and structural-repair interaction; +- the [internal source document](../internal/sources.md) for + prompt-definition role normalization and source-owned failures; +- the [internal model-client document](../internal/llm.md) only if its + effective-message input contract needs clarification; +- the [architecture policy](../policy/architecture.md) only if implementation + establishes a durable request-composition invariant not already covered by + the public-facade and application-neutral-boundary rules; +- the + [OpenAI-compatible integration contract](../integrations/openai-compatible-chat.md) + for supported provider-bound roles and provider rejection; and +- release guidance for the new minor version, including migration guidance for + consumers with nonstandard prompt-definition roles. + +The Go-only appended-message field does not belong in `docs/formats.md`. +However, the prompt-definition role contract does change and must be updated in +that canonical format reference. Profile and schema formats do not change. + +## Verification Expectations + +Tests protect the public composition and ownership contract rather than +private helper layout or exact diagnostic prose. Focused coverage establishes: + +- nil and empty appended slices preserve current behavior; +- ordinary rendered messages remain an exact prefix and appended messages + retain caller order; +- supported roles in both sources normalize by trimming and lowercasing, while + unsupported, `tool`, and `function` roles are rejected at the owning + boundary; +- message content, including empty and whitespace-only content, is preserved + exactly and receives no Promptkit-owned count or size restriction; +- `Prepare`, `PrepareExecution`, and `Run` agree on the combined sequence and + rendered prompt hash; +- `RunPrepared` uses its frozen appended-message snapshot after caller + mutation; +- `Details` and public conversions return independent copies; +- changing a normalized appended role, content, order, or cache control changes + the + rendered prompt hash without changing `PromptHash`; +- invalid appended roles, UTF-8, and cache controls fail as invalid requests + before model generation without echoing sensitive content; +- provider rejection of an otherwise structurally valid combined sequence + retains the ordinary generation-error contract; +- the initial model call receives the combined sequence; +- structural repair reconstructs the complete combined sequence before adding + Promptkit's repair turns; +- usage, capacity, cancellation, credentials, structured output, and error + identity retain their existing behavior; and +- `String` and `GoString` expose only a count for appended messages. + +One external-package contract test and focused internal runner tests should +own most of this matrix. Extend the maintained offline run example only if it +materially improves consumer understanding; do not turn the example into a +retry framework or duplicate the contract tests. + +## Acceptance Criteria + +- A consumer can append already-rendered messages through `RunRequest` without + defining a second prompt manifest or bypassing `Engine`. +- Normalized ordinary prompt messages remain an unchanged, ordered prefix. +- The final sequence is prepared, hashed, reported in prepared details, frozen, + executed, and repaired consistently across all engine workflows. +- Roles are normalized to and validated against the supported four-role + vocabulary at both prompt-definition and appended-message boundaries. +- Appended values are structurally validated, defensively copied, and excluded + from ordinary request formatting and error text without Promptkit-owned + count, size, token, or context-window limits. +- Promptkit structural repair treats the combined sequence as its original + prompt and retains all existing execution safeguards. +- Promptkit introduces no application-specific retry, validation, pipeline, or + terminal-policy concepts. +- Existing consumers that omit appended messages and use supported roles retain + current behavior; nonstandard prompt-definition roles are intentionally + rejected under the tightened minor-version contract. +- Canonical GoDoc and consumer/internal documentation describe the implemented + contract, and the complete maintainer validation workflow passes. + +## Non-Goals + +- A Promptkit-owned semantic validation framework. +- A generic outer retry orchestrator. +- Automatic interpretation or formatting of downstream validation errors. +- Prepending, inserting, replacing, or deleting configured prompt messages. +- Treating appended content as a template or file reference. +- Accumulating conversation state across `Run` calls. +- Freezing or reusing a base prompt across independent operations. +- A durable session or retry transcript store. +- PromptKit-owned message-count, byte-size, token, or context-window limits. +- Tool calls, tool results, deprecated function messages, or arbitrary custom + message roles. +- Provider-specific branching for Notarius. +- Changes to profile or schema file formats. + +## Alternatives Considered + +### Paired Initial And Correction Prompt Definitions + +A downstream application could register a second prompt definition that +duplicates the initial manifest and adds candidate and correction inputs. This +requires no Promptkit API change, but it duplicates prompt metadata and creates +drift across prompt versions, profile defaults, output contracts, assets, and +hashes. It also makes a generic request-composition need module-specific. + +### A Purpose-Specific Repair Request + +Promptkit could expose an API accepting `PreviousOutput` and +`ValidationErrors`, mirroring its internal structural repair request. This +would initially fit Notarius, but it would make Promptkit interpret downstream +policy and would conflate application semantic retries with Promptkit +structural repair. A generic append-only message contract is smaller and more +reusable. + +### Direct Execution Of A Mutated Prepared Prompt + +Promptkit could expose mutable prepared messages or accept a complete caller- +constructed prompt for execution. That would weaken definition ownership, +prepared-execution immutability, hashing, validation, and provenance, and would +make it easy to bypass source and preparation guarantees. The requested API +should compose messages before immutable preparation instead. + +### Direct Use Of The Model Client + +Notarius could invoke an injected Promptkit-compatible model client directly. +That bypasses the engine's profile resolution, backend admission, structured +output, repair, error mapping, and prepared-execution contracts. It is not an +acceptable downstream integration path. + +### A Semantic-Validation Callback + +Promptkit could accept an application callback that validates each candidate +and drives another generation inside one `Run`. That could retain one frozen +prompt and aggregate usage automatically, but it would make Promptkit own +callback lifecycle, semantic diagnostics, application retry policy, and the +boundary between typed domain values and raw model output. Keeping semantic +validation and its outer loop downstream leaves the public API smaller and the +library application-neutral. + +### A Reusable Prepared Conversation + +Promptkit could expose a reusable handle whose frozen base prompt accepts new +turns across multiple executions. That would guarantee a byte-identical prefix +and could support longer-lived chatbot workflows, but it would require a much +larger lifecycle, credential-retention, concurrency, source-freshness, and +history-ownership contract. The stateless append-only request primitive meets +the demonstrated need without precommitting to those policies. + +### Complete Caller-Supplied Or Mutable Message Sequences + +Promptkit could let a consumer replace, insert, or delete configured messages, +or execute an entirely caller-constructed sequence. That would weaken prompt +definition ownership, hashing, provenance, and the prepared-execution +boundary. Append-only composition provides the needed flexibility while +preserving configured messages as the canonical prefix. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md new file mode 100644 index 0000000..1528c42 --- /dev/null +++ b/docs/roadmap/implementation.md @@ -0,0 +1,436 @@ +# Appended Request Messages Implementation Plan + +## Purpose + +This document is the ordered implementation plan for the target state in +[appended-request-messages.md](appended-request-messages.md). It is written for +a coding agent implementing one stage per prompt, in order. The feature +roadmap owns intent, policy, acceptance criteria, and non-goals; this document +owns sequencing and concrete implementation decisions. + +The work adds one stateless, append-only request-composition primitive and +tightens prompt-definition roles to the same four-role vocabulary. It does not +add a conversation object, application validation callback, retry policy, +message limit, context estimator, or tool-call representation. + +## Cross-Stage Constraints + +Apply these constraints throughout every stage: + +- Preserve the architecture in + [docs/policy/architecture.md](../policy/architecture.md): the root package is + the public facade, `internal/domain` owns source-neutral message invariants, + source packages own source-specific error classification, and + `internal/usecase` owns composition and execution orchestration. +- Keep `RenderedMessage.Role` a string. Do not introduce a new public message + type, enum type, builder, conversation handle, callback, or alternate run + method. +- Support exactly `developer`, `system`, `user`, and `assistant`. Normalize a + role only by trimming surrounding Unicode whitespace with + `strings.TrimSpace` and lowercasing with `strings.ToLower`; do not alias one + role to another. +- Preserve message content exactly after confirming that request-supplied + content is valid UTF-8. Empty and whitespace-only appended content is valid + at the Promptkit boundary. Do not trim, template, load, truncate, estimate, + or otherwise transform it. +- Do not add message-count, byte-size, token, context-window, or retry limits. + An upstream rejection of a structurally valid request must continue through + the existing generation-error boundary. +- Never reproduce appended role text or content in errors, formatting, or new + logging. It is acceptable to identify an invalid message by zero-based index + and property name. +- Defensively copy every retained or returned message slice and every nested + `CacheControl` pointer. Prepared execution must remain an immutable snapshot. +- Keep `PromptHash` definition-only. Treat `RenderedPromptHash` as an opaque + equality value covering the effective session and complete effective + message sequence. +- Follow [docs/policy/testing.md](../policy/testing.md): put exhaustive + invariant matrices at their narrow owner, use external-package tests for the + public contract, avoid repeating the same matrix at every layer, and keep + all tests deterministic and offline. +- Do not create a release tag, push a release, or retire either roadmap during + implementation. Release preparation follows acceptance and review of the + completed feature. + +## Stage 1: Centralize Message Invariants And Tighten Prompt Roles + +### Objective + +Establish one source-neutral owner for chat-message role and cache-control +rules, then make prompt-definition loading and rendering publish only canonical +supported roles. This stage intentionally does not add the public request +field. + +### Implementation + +1. Add an `internal/domain` message-invariant file and define the untyped + string constants `RoleDeveloper`, `RoleSystem`, `RoleUser`, and + `RoleAssistant` with the exact values in the feature roadmap. +2. Add a role normalizer in `internal/domain` with the signature and behavior: + + ```go + func NormalizeMessageRole(role string) (string, error) + ``` + + It must reject invalid UTF-8, trim surrounding whitespace, lowercase the + result, accept exactly the four constants, and reject blank or unsupported + values. Its errors must describe the violated role rule without embedding + the supplied value. +3. Move the source-neutral cache-control normalization currently owned by + `internal/promptdef` into `internal/domain`: + + ```go + func NormalizeCacheControl(control *CacheControl) (*CacheControl, error) + ``` + + Preserve the existing contract: nil remains nil; type and TTL are trimmed; + type must be `ephemeral`; TTL must be empty or `1h`; the returned pointer is + newly allocated. Reject invalid UTF-8 before normalization and do not echo + invalid values. Keep the existing cache-control constants as the canonical + values. +4. Add domain-owned cloning helpers with these signatures: + + ```go + func CloneRenderedMessages(messages []RenderedMessage) []RenderedMessage + func ConcatRenderedMessages(prefix, suffix []RenderedMessage) []RenderedMessage + ``` + + The first returns a deep clone. The second allocates one result in prefix- + then-suffix order and deep-copies both inputs. Both must clone nested + cache-control pointers and preserve role, content, order, nil cache-control, + empty content, and whitespace exactly. Do not normalize or validate inside + a clone helper. +5. Refactor prompt-definition normalization in + `internal/promptdef/filesystem_repository.go` to call the domain role and + cache-control normalizers. Convert the source-only `cacheControlFile` into a + temporary `domain.CacheControl` before normalization; do not move YAML + source types into the domain package. Store the normalized role. Preserve + the existing prompt-definition error category, selected-file context, and + message index, while ensuring diagnostics do not reproduce an invalid role + value. +6. Make `internal/prompt/go_renderer.go` defensively call the same role + normalizer before publishing each rendered message. This protects the + provider-bound invariant even for an internal repository implementation + that did not originate in `internal/promptdef`. Retain the renderer's + existing error identity and message index. Continue to render content and + session templates exactly as before. +7. Replace any literal `"assistant"` and `"user"` roles in + `internal/usecase/repairer.go` with the domain constants. Do not otherwise + change repair behavior in this stage. + +### Tests + +1. Add a table at the domain owner covering: + + - all four accepted roles; + - surrounding whitespace and mixed-case normalization; + - blank, `tool`, `function`, and an arbitrary custom role; + - invalid UTF-8; and + - cache-control normalization, invalid values, and defensive-copy behavior. + +2. Add focused prompt-definition repository coverage proving that a + mixed-case/whitespace role is published canonically and that an unsupported + role is an invalid prompt definition with its existing public-facing error + category. Do not repeat the entire domain role table. +3. Add focused renderer coverage proving defensive canonicalization and + rejection for a directly supplied internal definition. Preserve all + existing rendering and cancellation tests. +4. Run the tests for `internal/domain`, `internal/promptdef`, + `internal/prompt`, and `internal/usecase`, then run `go test ./...` before + completing the stage. + +### Completion Criteria + +- Every definition-supplied role reaching a rendered prompt is one of the + canonical constants. +- Prompt-definition and renderer failures retain their owning error identities. +- Cache-control rules and message deep-copy behavior have one source-neutral + implementation. +- Existing supported prompt definitions and structural repair behavior remain + unchanged. + +## Stage 2: Add The Public Request Contract And Boundary Validation + +### Objective + +Expose the smallest public API for appended messages and convert it into a +validated, caller-independent domain request before any source or model work. +The runner may still ignore the new domain field until Stage 3. + +### Implementation + +1. Add `AppendedMessages []RenderedMessage` to both the public and domain + `RunRequest` values. Public GoDoc must state that values are already + rendered, are appended after all definition messages, are not templated or + file-resolved, preserve content exactly, and treat nil and empty slices as + equivalent. +2. Add a small root-package file, rather than importing `internal/domain` into + the general public types file solely for constants, and publish untyped + aliases to the domain constants: + + ```go + const ( + RoleDeveloper = domain.RoleDeveloper + RoleSystem = domain.RoleSystem + RoleUser = domain.RoleUser + RoleAssistant = domain.RoleAssistant + ) + ``` + + Give every exported declaration exact GoDoc. Do not introduce a named role + type; assignments and comparisons with ordinary strings must remain + frictionless. +3. Generalize `RenderedMessage` GoDoc so it describes any prepared, + provider-bound text chat message rather than only a definition-supplied + message. Document the four supported roles and refer callers to the + constants. +4. Add one conversion helper in `convert.go` that processes appended messages + in order. For each message it must: + + - reject invalid UTF-8 in content without reproducing it; + - normalize and validate the role through + `domain.NormalizeMessageRole`; + - normalize and defensively copy cache control through + `domain.NormalizeCacheControl`; and + - return a newly allocated domain slice with independently allocated nested + cache-control pointers. + + Wrap failures with the zero-based appended-message index and property name, + but not the failing value. +5. Call that helper from `toDomainRunRequest` and store the result on the + domain request. Because `Prepare`, `PrepareExecution`, and `Run` already use + this shared conversion boundary, conversion failures must emerge as the + existing public `ErrInvalidRequest` before source resolution, preparation, + capacity admission, or model generation. +6. Extend `RunRequest.String` and `RunRequest.GoString` through their shared + redacted formatter to include only `AppendedMessages:`. Do not emit + roles, content, cache-control values, or a serialized message value. + +### Tests + +1. Add an external-package public contract test for the exact constant values + and for ordinary string compatibility. +2. Put the request-message validation matrix at the public conversion owner. + Cover supported normalization, invalid UTF-8 in role and content, + unsupported roles, invalid cache control, empty content, whitespace-only + content, and message-indexed errors that omit the original role and content. +3. Add a small parity test proving that `Prepare`, `PrepareExecution`, and + `Run` all classify an invalid appended message as `ErrInvalidRequest` and do + not invoke configured source/model fakes. Do not repeat the complete + invalid-value matrix through all three operations. +4. Extend the existing request-formatting test so both `String` and `GoString` + expose the count and cannot contain distinctive role/content values. +5. Run root-package tests and `go test ./...` before completing the stage. + +### Completion Criteria + +- The only new public surface is one `RunRequest` field and four untyped string + constants. +- Every public operation rejects malformed appended messages consistently and + before side effects. +- Request formatting remains redacted. +- Public-to-domain conversion owns an independent, canonical snapshot of the + request values. + +## Stage 3: Compose, Freeze, Hash, Execute, And Repair The Effective Prompt + +### Objective + +Make appended messages part of the effective rendered prompt everywhere after +ordinary rendering, including immutable preparation, client execution, +structural repair, and opaque rendered-prompt identity. + +### Implementation + +1. In the shared completion phase in `internal/usecase/runner.go`, append the + already-normalized domain request messages immediately after ordinary + rendering and application of the direct-session override. Use the domain + concatenation helper so the resulting `RenderedPrompt.Messages` owns a deep + copy of both the ordinary prefix and appended suffix. +2. Use that combined rendered prompt for all subsequent work: rendered-prompt + hashing, `PreparedRun.Messages`, `PreparedExecution` details, the initial + generation request, `RunResult.RenderedPromptHash`, and the original-message + input to structural repair. Do not alter `PromptHash` or input hashes. +3. Refactor `clonePreparedRun` and any other prepared-message clone path to use + the shared domain deep-clone helper. Keep public conversion helpers as the + public/domain boundary, but remove duplicate internal cache-control pointer + cloning where the new domain helper is the appropriate owner. +4. Ensure `PreparedExecution` freezes the combined messages. The opaque + retained value, every call to `Details`, and a later `RunPrepared` must have + independent message slices and cache-control pointers. Caller mutation of + the original request or of returned details must not alter execution. +5. In the default structural repairer, start every repair request with a deep + copy of the complete combined original sequence. Append only the latest + nonempty candidate with `domain.RoleAssistant`, followed by the corrective + message with `domain.RoleUser`. Preserve the existing empty-candidate rule, + diagnostic bound, retry budget, usage accounting, capacity path, + cancellation, target, session, credential, and structured-output behavior. +6. Replace the delimiter-based `hashRenderedPrompt` encoding with a versioned, + length-framed binary encoding. Use this exact field order: + + 1. the fixed bytes `promptkit/rendered-prompt/v2\x00`; + 2. the effective session ID as a `uint64` big-endian byte length followed by + its bytes; + 3. the message count as `uint64` big-endian; + 4. for each message in order, role and content as separately length-prefixed + byte strings; + 5. one cache-control presence byte; and + 6. when present, type and TTL as separately length-prefixed byte strings. + + Hash the resulting framing with SHA-256 and retain the lowercase hexadecimal + public representation. The terminating NUL is part of the fixed marker and + must be written before the first length. Use a fixed eight-byte buffer with + `binary.BigEndian.PutUint64`; do not use `binary.Write`, JSON, `fmt`, + separators, or an input-sized staging buffer. The returned value is opaque, + so do not preserve the old digest. This change deliberately prevents caller + content from colliding with structural separators and streams in constant + auxiliary space. +7. Confirm that the built-in OpenAI-compatible client forwards the canonical + role and exact content without provider-specific translation. No new + provider role branching or proactive context validation is permitted. + +### Tests + +1. At the runner owner, prove that ordinary rendered messages are an exact + prefix, appended messages retain their order and content, and nil/empty + suffixes retain current behavior. +2. Prove that `Prepare`, `PrepareExecution`, and `Run` agree on the complete + sequence and rendered-prompt hash, and that the injected model client sees + exactly that sequence. +3. Prove immutable ownership by mutating the original appended slice and its + cache-control pointer after `PrepareExecution`, then mutating a value from + `Details`; neither mutation may affect later details or `RunPrepared`. +4. Extend hash tests to establish: + + - normalized-equivalent roles yield the same rendered hash; + - changing role, content, order, cache-control presence/value, or session + changes the rendered hash; + - appended-message changes do not change `PromptHash`; and + - the one-message sequence `{role: "user", content: + "x\n---\nassistant\ny"}` hashes differently from the two-message sequence + `{user, "x"}, {assistant, "y"}`, even though the legacy separator + encoding could not distinguish them. + + Test observable relationships, not a golden SHA-256 digest. +5. Extend repair tests to prove the exact order: ordinary prefix, consumer + suffix, latest structurally invalid assistant candidate when nonempty, and + Promptkit corrective user message. Also prove that a later repair begins + again from the complete original combined sequence rather than accumulating + prior repair turns. +6. Add one focused OpenAI-compatible request test covering canonical role + forwarding, including `developer`, and exact content preservation. Do not + duplicate all HTTP client or role-validation tests. +7. Exercise one otherwise valid combined request that the fake provider rejects + and confirm the existing `GenerationError` status/provider-detail contract + remains intact. Reuse the existing generation-error test scaffolding rather + than creating a second status matrix. +8. Run tests for `internal/domain`, `internal/usecase`, `internal/llm`, and the + root package, then run `go test ./...` and `go test -race ./...` before + completing the stage. + +### Completion Criteria + +- Every prepared, inspected-prepared, executed, and repaired message sequence + uses the same ordered combined prompt. +- Prepared execution remains immutable across every caller-visible ownership + boundary. +- Rendered-prompt hashing is unambiguous, streaming, and sensitive to every + contracted field while `PromptHash` remains definition-only. +- Existing repair, usage, capacity, cancellation, credential, validation, and + provider-error behavior is preserved. + +## Stage 4: Publish Canonical Documentation And Complete Validation + +### Objective + +Bring every canonical documentation owner into line with the implemented API, +record the intentional prompt-format compatibility change, and run the full +repository validation workflow. + +### Documentation + +1. Finish the exact root-package GoDoc for `RunRequest.AppendedMessages`, the + four role constants, `RenderedMessage`, `PreparedRun.Messages`, and the + relevant `Engine` methods. Document normalization, ordering, ownership, + empty-content behavior, error identity, and statelessness at the narrowest + declaration that owns each rule. +2. Update [docs/formats.md](../formats.md) as the canonical prompt-file + contract: + + - enumerate the four accepted message roles; + - state trim-and-lowercase normalization; + - state that blank, custom, `tool`, and `function` roles are invalid; and + - identify this as an intentional tightening from the prior nonblank-string + rule for consumers migrating to the next minor release. + + Do not document the Go-only `AppendedMessages` field as a file-format field. +3. Add one concise, application-neutral example to + [docs/consumers/pkg-promptkit.md](../consumers/pkg-promptkit.md) showing a + previous assistant response and a corrective user message passed through + `AppendedMessages`. Explain that the messages are already rendered and may + contain sensitive data, Promptkit remains stateless, each call re-resolves + current sources, the consumer owns any semantic retry budget, and + `PrepareExecution` plus opaque hash comparison is available when a + pre-execution equality check is required. +4. Update [docs/internal/sources.md](../internal/sources.md) with shared role + normalization and source-owned failure behavior. Update + [docs/internal/runner.md](../internal/runner.md) with append ordering, + immutable preparation, the versioned framed hash, and structural-repair + interaction. Update [docs/internal/llm.md](../internal/llm.md) only if needed + to clarify that the client receives canonical text messages and leaves + supported-role/context rejection to the provider. +5. Update the + [OpenAI-compatible integration contract](../integrations/openai-compatible-chat.md) + to list the four provider-bound roles, state that Promptkit sends them + without translation, and explain that a backend/model rejection follows the + normal provider-error path. Keep tool/function payloads outside the + supported wire contract. +6. Update [docs/policy/architecture.md](../policy/architecture.md) only if the + implementation created a durable architectural invariant not already + covered by domain ownership, the public facade, and the + application-neutral boundary. Do not add feature-level API details to + policy. No new internal package is expected, so do not change the internal + package overview merely because a file was added. +7. Do not create a release tag in this stage. Record in the final handoff that + the next release must be a minor version and that its release note must + summarize both the new append API and migration from nonstandard + prompt-definition roles, linking to GoDoc and `docs/formats.md` for detail. + Create the release document and README link only as part of the repository's + subsequent release-preparation task. + +### Final Verification + +1. Review new tests against [docs/policy/testing.md](../policy/testing.md). + Consolidate repeated setup and assertions only where it clarifies behavior; + do not replace behavioral tests with helper-implementation tests. +2. Run the complete maintainer validation workflow from + [docs/development.md](../development.md), including: + + - all ordinary and race-enabled Go tests; + - `go vet` and the build; + - both maintained examples; + - Go and Markdown formatting/link checks; + - diff, ignored-file, workspace, vendoring, module-replacement, and + credential hygiene checks. + +3. Review the final diff to confirm that no provider-specific Notarius concept, + conversation state, message limit, content logging, new credential handling, + or unrelated refactor entered the change. +4. Leave both roadmap documents in place for post-implementation review. Their + retirement belongs to a later cleanup after the feature is accepted. + +### Completion Criteria + +- Canonical GoDoc, format, consumer, internal, and integration documentation + agree with the implemented behavior and do not duplicate ownership. +- Migration impact for unsupported legacy prompt roles is explicit. +- The complete maintainer workflow passes from a clean, supported Go setup. +- The repository is ready for an implementation review followed by minor + release preparation. + +## Open Questions + +None. The feature roadmap and this plan resolve the public API shape, role +vocabulary, normalization, content handling, ownership, composition order, +hash framing, repair interaction, provider boundary, documentation ownership, +and release compatibility policy required for implementation.