From 174516cb398b85bfd4197689c59fd9249bf6ddb7 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 26 Aug 2026 02:19:44 +0000 Subject: [PATCH] Document appended request messages --- docs/consumers/pkg-promptkit.md | 20 ++++++++++++++++++++ docs/formats.md | 8 +++++++- docs/integrations/openai-compatible-chat.md | 6 ++++++ docs/internal/llm.md | 5 +++-- docs/internal/runner.md | 15 ++++++++++----- docs/internal/sources.md | 11 +++++++++-- docs/roadmap/implementation.md | 2 ++ engine.go | 8 ++++++-- message_roles.go | 8 ++++---- prepared_execution.go | 4 +++- types.go | 17 ++++++++++------- 11 files changed, 80 insertions(+), 24 deletions(-) diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index c67de18..2a298c2 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -196,6 +196,26 @@ validity is not evidence of factual or domain correctness. See the [`OutputContract` GoDoc](../../types.go) for the exact budget and eligibility rules. +### Append Already-Rendered Messages + +An application can include an earlier assistant response and its own corrective +instruction in a fresh request without changing the configured prompt: + +```go +request.AppendedMessages = []promptkit.RenderedMessage{ + {Role: promptkit.RoleAssistant, Content: previousResponse}, + {Role: promptkit.RoleUser, Content: correction}, +} +result, err := engine.Run(ctx, request) +``` + +These messages are already rendered: Promptkit does not template or resolve +files in them, and they can contain sensitive model output or application +feedback. Promptkit remains stateless; every `Run` call re-resolves its current +sources and the application owns any semantic retry budget. When a +pre-execution equality check is required, use `PrepareExecution` and compare +its opaque rendered-prompt hash before invoking `RunPrepared`. + ## Inputs, Profiles, And Overrides Use `File`, `Inline`, or `InlineWithURI` to construct request inputs. A request diff --git a/docs/formats.md b/docs/formats.md index 56544d7..05f201a 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -82,7 +82,13 @@ allowed. ### Messages And Templates -Each message has a non-empty `role` and exactly one of: +Each message has a `role` that Promptkit trims and lowercases. It must then be +exactly one of `developer`, `system`, `user`, or `assistant`; blank, custom, +`tool`, and `function` roles are invalid. This intentionally tightens the +previous nonblank-string rule. Consumers migrating to the next minor release +must update any nonstandard prompt-definition roles before upgrading. + +Each message also has exactly one of: - `content`, containing an inline Go template; or - `content_file`, naming a file whose contents are the Go template. diff --git a/docs/integrations/openai-compatible-chat.md b/docs/integrations/openai-compatible-chat.md index ee3eec6..3613baa 100644 --- a/docs/integrations/openai-compatible-chat.md +++ b/docs/integrations/openai-compatible-chat.md @@ -55,6 +55,12 @@ Each ordinary message contains its `role` and string `content`. A cache-controlled message instead uses a text content block containing `type`, `text`, and `cache_control`; an empty cache-control TTL is omitted. +Promptkit sends only `developer`, `system`, `user`, and `assistant` roles and +does so without provider-specific translation. Tool and deprecated function +payloads are outside this text-message contract. A backend or model that +rejects an otherwise supported role or context returns its ordinary provider +error, which follows the normal generation-error path. + The effective direct or prompt-rendered session ID is trimmed, limited to 256 Unicode code points, and sent when nonempty as top-level `session_id`. It is never also sent as a session header. diff --git a/docs/internal/llm.md b/docs/internal/llm.md index 1803ff6..c64282a 100644 --- a/docs/internal/llm.md +++ b/docs/internal/llm.md @@ -21,8 +21,9 @@ uses internal domain values for rendered prompts, execution targets, structured output, responses, and token usage. The runner supplies a fully resolved target after applying backend, profile, -and request precedence. The client uses its endpoint, credential metadata, -generation fields, and extra parameters. `BackendID` remains routing metadata +and request precedence, plus canonical provider-bound text messages. The +client uses its endpoint, credential metadata, generation fields, and extra +parameters. `BackendID` remains routing metadata for the generation boundary and is not mapped into the provider payload. Construction trims and validates a nonempty configured base URL and clones any diff --git a/docs/internal/runner.md b/docs/internal/runner.md index 9acdd81..46d51fe 100644 --- a/docs/internal/runner.md +++ b/docs/internal/runner.md @@ -84,7 +84,8 @@ profile, or backend: schema metadata from it when required; 2. load and hash input artifacts; 3. render messages and the prompt-defined session; -4. apply any direct session ID; +4. apply any direct session ID, then append already-normalized request messages + after the rendered definition messages; 5. hash the effective rendered prompt; and 6. construct the prepared value and preparation timing. @@ -113,7 +114,10 @@ the prompt session template, and is applied after ordinary message rendering. A blank direct value retains prompt-template behavior. The runner clears the template only on a value copy of the definition, so the definition hash always describes the original source while the rendered-prompt hash includes the -effective direct or rendered session. +effective direct or rendered session and the complete effective message +sequence. The rendered-prompt hash uses a versioned, length-framed SHA-256 +encoding of that session, every role and content value, and cache-control +presence and values; its hexadecimal value is opaque. The registry is read-only after engine construction. Concurrent `Prepare` and `Run` calls resolve independent defensive backend values and keep all @@ -146,9 +150,10 @@ each actual generation call. After a failed `basic`, JSON, or JSON Schema validation with a positive frozen budget, the installed repairer can make a bounded corrective call. Each request -starts with a fresh copy of the complete original rendered messages, includes -only the latest nonempty candidate as an assistant message, and appends one -corrective user message. Empty candidates omit that assistant message. The +starts with a fresh copy of the complete effective message sequence (the +configured prefix followed by the request suffix), includes only the latest +nonempty candidate as an assistant message, and appends one corrective user +message. Empty candidates omit that assistant message. The correction carries validation diagnostics as JSON data bounded to 64 KiB; the full diagnostics remain in the validation result. diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 7f6e280..cac79ee 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -21,6 +21,12 @@ paths, content opening, and root containment. Each lookup remains a point-in-time scan: definitions and catalogs are not cached, and file-backed message content is opened only for the exact selected candidate. +Message roles are normalized through the shared domain owner by trimming +Unicode whitespace and lowercasing. Only `developer`, `system`, `user`, and +`assistant` are published; invalid roles remain selected prompt-definition +failures rather than becoming request errors. Cache-control metadata uses the +same shared domain normalization and defensive-copy rule. + Operating-system sources enforce containment against canonical roots and targets so symlinks cannot escape. Injected `fs.FS` sources enforce containment in their clean relative path namespace. A single-file source uses the selected @@ -122,8 +128,9 @@ Session and message parsing and execution remain synchronous. The renderer checks cancellation before and after each parse and execution boundary, between artifact conversion chunks, around each message, and before publishing the complete prompt. It cannot interrupt template work already in progress and -never publishes a partial prompt after observing cancellation. It carries -message roles, session IDs, and cache control into the rendered prompt. The +never publishes a partial prompt after observing cancellation. It validates and +canonicalizes message roles before carrying roles, session IDs, and cache +control into the rendered prompt. The [renderer tests](../../internal/prompt/renderer_test.go) own rendering behavior. ## Schemas And Output Validation diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 41175ac..90f0571 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -347,6 +347,8 @@ structural repair, and opaque rendered-prompt identity. ## Stage 4: Publish Canonical Documentation And Complete Validation +**Status:** Complete + ### Objective Bring every canonical documentation owner into line with the implemented API, diff --git a/engine.go b/engine.go index 8eef840..cbea102 100644 --- a/engine.go +++ b/engine.go @@ -571,6 +571,8 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile } // Prepare resolves and renders a prompt request without calling an LLM. +// It appends any validated RunRequest.AppendedMessages after rendered +// definition messages in the returned caller-owned snapshot. // // Prepare selects the prompt and profile, resolves any selected backend and // effective execution settings, resolves the output contract, loads and hashes @@ -605,7 +607,8 @@ func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, err } // PrepareExecution completely prepares a prompt request without calling the -// configured LLMClient or reserving backend admission capacity. +// configured LLMClient or reserving backend admission capacity. Validated +// RunRequest.AppendedMessages are included in the frozen effective messages. // // The returned opaque handle is bound to this Engine and permits one // [Engine.RunPrepared] invocation. Preparation freezes the selected sources, @@ -637,7 +640,8 @@ func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*Prepare } // Run prepares a request, invokes the configured LLMClient, and validates the -// generated output. +// generated output. Each call resolves current sources and composes a fresh, +// stateless effective prompt with any validated RunRequest.AppendedMessages. // // A content-validation failure is a successful run whose // RunResult.Validation has Status ValidationFailed. When its output contract diff --git a/message_roles.go b/message_roles.go index cb691d3..d3eedc4 100644 --- a/message_roles.go +++ b/message_roles.go @@ -3,12 +3,12 @@ package promptkit import "gitea.maximumdirect.net/eric/promptkit/internal/domain" const ( - // RoleDeveloper identifies a developer instruction message. + // RoleDeveloper identifies a provider-bound developer instruction message. RoleDeveloper = domain.RoleDeveloper - // RoleSystem identifies a system instruction message. + // RoleSystem identifies a provider-bound system instruction message. RoleSystem = domain.RoleSystem - // RoleUser identifies a user message. + // RoleUser identifies a provider-bound user message. RoleUser = domain.RoleUser - // RoleAssistant identifies an assistant message. + // RoleAssistant identifies a provider-bound assistant message. RoleAssistant = domain.RoleAssistant ) diff --git a/prepared_execution.go b/prepared_execution.go index f0f8a9d..d7a9c84 100644 --- a/prepared_execution.go +++ b/prepared_execution.go @@ -17,7 +17,9 @@ type PreparedExecution struct { // Details returns a fresh caller-owned, credential-redacted copy of the // prepared request details. Mutating the result cannot affect execution or a -// later Details call. Details remains available after execution or discard. +// later Details call. Its complete effective message content, including any +// appended request messages, remains subject to the caller's data-handling +// policy. Details remains available after execution or discard. // // A nil receiver or zero-value PreparedExecution returns a zero [PreparedRun]. func (p *PreparedExecution) Details() PreparedRun { diff --git a/types.go b/types.go index 15a31bf..9d05100 100644 --- a/types.go +++ b/types.go @@ -125,10 +125,11 @@ type RunRequest struct { Validation *OutputContract // AppendedMessages are already-rendered messages appended after every prompt // definition message. Promptkit neither templates nor resolves files in - // them, and preserves valid content exactly. Nil and empty slices are - // equivalent. Prepare, PrepareExecution, and Run validate and copy the - // messages before source or model work; malformed values return an error - // matching ErrInvalidRequest. + // them, and preserves valid content exactly. Roles are trimmed and + // lowercased, then must be [RoleDeveloper], [RoleSystem], [RoleUser], or + // [RoleAssistant]. Nil and empty slices are equivalent. Prepare, + // PrepareExecution, and Run validate and copy the messages before source or + // model work; malformed values return an error matching ErrInvalidRequest. AppendedMessages []RenderedMessage } @@ -632,13 +633,15 @@ type RenderedPrompt struct { // SessionID is the optional effective direct or rendered session // identifier supplied to the model client. SessionID string `json:"session_id,omitempty"` - // Messages contains rendered messages in definition order. + // Messages contains the frozen effective messages: definition messages in + // their rendered order followed by any RunRequest.AppendedMessages. Messages []RenderedMessage `json:"messages"` } // RenderedMessage is a prepared, provider-bound text chat message and has a -// stable JSON representation. Its role must be one of [RoleDeveloper], -// [RoleSystem], [RoleUser], or [RoleAssistant]. +// stable JSON representation. At a request boundary its role is trimmed and +// lowercased, then must be one of [RoleDeveloper], [RoleSystem], [RoleUser], +// or [RoleAssistant]. type RenderedMessage struct { // Role is the provider-bound chat role. Role string `json:"role"`