22 KiB
Appended Request Messages Implementation Plan
Purpose
This document is the ordered implementation plan for the target state in 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: the root package is
the public facade,
internal/domainowns source-neutral message invariants, source packages own source-specific error classification, andinternal/usecaseowns composition and execution orchestration. - Keep
RenderedMessage.Rolea string. Do not introduce a new public message type, enum type, builder, conversation handle, callback, or alternate run method. - Support exactly
developer,system,user, andassistant. Normalize a role only by trimming surrounding Unicode whitespace withstrings.TrimSpaceand lowercasing withstrings.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
CacheControlpointer. Prepared execution must remain an immutable snapshot. - Keep
PromptHashdefinition-only. TreatRenderedPromptHashas an opaque equality value covering the effective session and complete effective message sequence. - Follow docs/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
Status: Complete
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
-
Add an
internal/domainmessage-invariant file and define the untyped string constantsRoleDeveloper,RoleSystem,RoleUser, andRoleAssistantwith the exact values in the feature roadmap. -
Add a role normalizer in
internal/domainwith the signature and behavior: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.
-
Move the source-neutral cache-control normalization currently owned by
internal/promptdefintointernal/domain: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 or1h; 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. -
Add domain-owned cloning helpers with these signatures:
func CloneRenderedMessages(messages []RenderedMessage) []RenderedMessage func ConcatRenderedMessages(prefix, suffix []RenderedMessage) []RenderedMessageThe 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.
-
Refactor prompt-definition normalization in
internal/promptdef/filesystem_repository.goto call the domain role and cache-control normalizers. Convert the source-onlycacheControlFileinto a temporarydomain.CacheControlbefore 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. -
Make
internal/prompt/go_renderer.godefensively 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 ininternal/promptdef. Retain the renderer's existing error identity and message index. Continue to render content and session templates exactly as before. -
Replace any literal
"assistant"and"user"roles ininternal/usecase/repairer.gowith the domain constants. Do not otherwise change repair behavior in this stage.
Tests
-
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.
-
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.
-
Add focused renderer coverage proving defensive canonicalization and rejection for a directly supplied internal definition. Preserve all existing rendering and cancellation tests.
-
Run the tests for
internal/domain,internal/promptdef,internal/prompt, andinternal/usecase, then rungo 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
Status: Complete
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
-
Add
AppendedMessages []RenderedMessageto both the public and domainRunRequestvalues. 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. -
Add a small root-package file, rather than importing
internal/domaininto the general public types file solely for constants, and publish untyped aliases to the domain constants: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.
-
Generalize
RenderedMessageGoDoc 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. -
Add one conversion helper in
convert.gothat 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.
-
Call that helper from
toDomainRunRequestand store the result on the domain request. BecausePrepare,PrepareExecution, andRunalready use this shared conversion boundary, conversion failures must emerge as the existing publicErrInvalidRequestbefore source resolution, preparation, capacity admission, or model generation. -
Extend
RunRequest.StringandRunRequest.GoStringthrough their shared redacted formatter to include onlyAppendedMessages:<count>. Do not emit roles, content, cache-control values, or a serialized message value.
Tests
- Add an external-package public contract test for the exact constant values and for ordinary string compatibility.
- 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.
- Add a small parity test proving that
Prepare,PrepareExecution, andRunall classify an invalid appended message asErrInvalidRequestand do not invoke configured source/model fakes. Do not repeat the complete invalid-value matrix through all three operations. - Extend the existing request-formatting test so both
StringandGoStringexpose the count and cannot contain distinctive role/content values. - Run root-package tests and
go test ./...before completing the stage.
Completion Criteria
- The only new public surface is one
RunRequestfield 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
-
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 resultingRenderedPrompt.Messagesowns a deep copy of both the ordinary prefix and appended suffix. -
Use that combined rendered prompt for all subsequent work: rendered-prompt hashing,
PreparedRun.Messages,PreparedExecutiondetails, the initial generation request,RunResult.RenderedPromptHash, and the original-message input to structural repair. Do not alterPromptHashor input hashes. -
Refactor
clonePreparedRunand 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. -
Ensure
PreparedExecutionfreezes the combined messages. The opaque retained value, every call toDetails, and a laterRunPreparedmust have independent message slices and cache-control pointers. Caller mutation of the original request or of returned details must not alter execution. -
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 withdomain.RoleUser. Preserve the existing empty-candidate rule, diagnostic bound, retry budget, usage accounting, capacity path, cancellation, target, session, credential, and structured-output behavior. -
Replace the delimiter-based
hashRenderedPromptencoding with a versioned, length-framed binary encoding. Use this exact field order:- the fixed bytes
promptkit/rendered-prompt/v2\x00; - the effective session ID as a
uint64big-endian byte length followed by its bytes; - the message count as
uint64big-endian; - for each message in order, role and content as separately length-prefixed byte strings;
- one cache-control presence byte; and
- 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 usebinary.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. - the fixed bytes
-
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
-
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.
-
Prove that
Prepare,PrepareExecution, andRunagree on the complete sequence and rendered-prompt hash, and that the injected model client sees exactly that sequence. -
Prove immutable ownership by mutating the original appended slice and its cache-control pointer after
PrepareExecution, then mutating a value fromDetails; neither mutation may affect later details orRunPrepared. -
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.
-
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.
-
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. -
Exercise one otherwise valid combined request that the fake provider rejects and confirm the existing
GenerationErrorstatus/provider-detail contract remains intact. Reuse the existing generation-error test scaffolding rather than creating a second status matrix. -
Run tests for
internal/domain,internal/usecase,internal/llm, and the root package, then rungo test ./...andgo 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
PromptHashremains 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
-
Finish the exact root-package GoDoc for
RunRequest.AppendedMessages, the four role constants,RenderedMessage,PreparedRun.Messages, and the relevantEnginemethods. Document normalization, ordering, ownership, empty-content behavior, error identity, and statelessness at the narrowest declaration that owns each rule. -
Update docs/formats.md as the canonical prompt-file contract:
- enumerate the four accepted message roles;
- state trim-and-lowercase normalization;
- state that blank, custom,
tool, andfunctionroles 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
AppendedMessagesfield as a file-format field. -
Add one concise, application-neutral example to docs/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, andPrepareExecutionplus opaque hash comparison is available when a pre-execution equality check is required. -
Update docs/internal/sources.md with shared role normalization and source-owned failure behavior. Update docs/internal/runner.md with append ordering, immutable preparation, the versioned framed hash, and structural-repair interaction. Update docs/internal/llm.md only if needed to clarify that the client receives canonical text messages and leaves supported-role/context rejection to the provider.
-
Update the OpenAI-compatible integration contract 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.
-
Update docs/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.
-
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.mdfor detail. Create the release document and README link only as part of the repository's subsequent release-preparation task.
Final Verification
-
Review new tests against docs/policy/testing.md. Consolidate repeated setup and assertions only where it clarifies behavior; do not replace behavioral tests with helper-implementation tests.
-
Run the complete maintainer validation workflow from docs/development.md, including:
- all ordinary and race-enabled Go tests;
go vetand the build;- both maintained examples;
- Go and Markdown formatting/link checks;
- diff, ignored-file, workspace, vendoring, module-replacement, and credential hygiene checks.
-
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.
-
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.