12 KiB
Executable Preparation Handles
Status: Accepted.
Purpose
Allow a consumer to prepare one exact Promptkit execution, inspect and retain its credential-redacted public preparation details, and later execute that already-prepared work without resolving or rendering the request again.
This provides a supported preflight-before-generation boundary for
Weatherreporter
and removes the duplicate Prepare-then-Run workaround described by
Notarius.
Motivation
Engine.Prepare currently returns the provenance and rendered details that
consumers need for debugging, persistence, and preflight checks. Engine.Run
then performs its own preparation before generation. A consumer that needs
both values must therefore prepare the same logical request twice.
That workaround duplicates source loading, input hashing, schema work, and template rendering. It also permits filesystem-backed prompts, profiles, schemas, or inputs to change between the public preparation and the preparation that actually produces the result.
Promptkit already owns the complete preparation and execution pipeline. An
opt-in prepared-execution handle should expose the missing boundary without
putting rendered content into every RunResult or moving persistence policy
into the library.
Consumer Workflow
The target public workflow is:
prepared, err := engine.PrepareExecution(ctx, request)
if err != nil {
// Handle preparation failure.
return
}
defer prepared.Discard()
details := prepared.Details()
// Persist or inspect a consumer-selected safe subset of details.
result, err := engine.RunPrepared(ctx, prepared)
The target public surface is:
type PreparedExecution struct {
// Opaque Promptkit-owned state.
}
func (e *Engine) PrepareExecution(
ctx context.Context,
req RunRequest,
) (*PreparedExecution, error)
func (p *PreparedExecution) Details() PreparedRun
func (p *PreparedExecution) Discard()
func (e *Engine) RunPrepared(
ctx context.Context,
prepared *PreparedExecution,
) (*RunResult, error)
The declarations and GoDoc will own the exact implemented contract. The
important public shape is an opaque handle, caller-owned PreparedRun
details, an explicit discard operation, and execution through the engine that
created the handle.
Prepared Snapshot
PrepareExecution performs complete preparation without model generation or
backend-capacity admission. It applies the same request validation, source
precedence, backend and profile resolution, credential requirement checks,
output-contract resolution, artifact loading, hashing, schema loading,
session resolution, and rendering behavior as Prepare.
A successful handle freezes all source-derived state required for later execution, including:
- the selected prompt definition, profile, and backend identity;
- the complete effective execution target and request-field presence;
- rendered messages and effective session ID;
- prompt, rendered-prompt, and input hashes;
- the effective output contract and provider-facing structured-output constraint; and
- private validation state sufficient to validate generated output without
reopening schema files or
fs.FSresources, including required schema references.
After PrepareExecution succeeds, changes to prompt, profile, schema, input,
or request-owned data cannot change what RunPrepared sends to the model or
how it validates the generated output.
The handle retains an internal snapshot independent from values returned by
Details. Mutating a returned PreparedRun, its maps, slices, messages, or
schema values does not affect later execution. Each Details call returns a
fresh caller-owned copy under the existing PreparedRun ownership and stable
JSON rules.
Handle Lifecycle
A PreparedExecution is:
- created only by a successful
PrepareExecutioncall; - bound to the exact
Enginethat created it; - valid for one
RunPreparedinvocation; - safe for repeated
Detailscalls; - intentionally opaque and without a supported JSON representation; and
- in-process state rather than a durable or restartable job.
RunPrepared atomically claims a valid handle before beginning the execution
attempt. A second or concurrent invocation fails without starting another
execution, including when the first invocation ended in cancellation, capacity
rejection, generation failure, or operational validation failure. Copies of
the public handle share the same one-attempt state and cannot bypass this rule.
A nil, zero-value, foreign-engine, discarded, already-claimed, or already-used
handle is invalid. RunPrepared reports these lifecycle errors through the
ordinary public invalid-request category. A failed foreign-engine invocation
does not consume a handle that remains valid for its owning engine.
Discard idempotently makes an unclaimed handle unavailable for execution and
drops Promptkit's references to secret-bearing or execution-only state.
RunPrepared performs the same cleanup automatically after claiming a handle.
Credential-redacted public preparation details remain available after discard,
success, or failure so consumers can retain diagnostics. Promptkit does not
promise secure erasure of Go string memory.
Lifecycle transitions are concurrency-safe. When RunPrepared and Discard
race, exactly one claims the ready handle. Discard is not an execution
cancellation mechanism and does not interrupt an attempt that has already
claimed the handle; consumers cancel that attempt through its context.
Credentials And Sensitive Data
Details has the same security contract as PreparedRun: it can contain
rendered messages, schemas, identifiers, and hashes, but never a resolved API
key value. Consumers remain responsible for selecting, redacting, storing, and
retaining any persisted preparation material.
A direct RunRequest.APIKey is retained only in opaque execution state until
the handle is run or discarded. It is never added to details, hashes, JSON,
String, or GoString output.
An environment-variable name is frozen as part of the effective target, but
its credential value is not captured for the lifetime of the handle.
PrepareExecution applies the existing preparation-time availability check.
RunPrepared rechecks availability before admission, and the selected model
client uses the environment value visible during execution. This preserves
current secret ownership and avoids retaining an environment credential while
a consumer persists preflight material.
The opaque handle must not expose retained request data or credentials through default formatting, JSON, or error messages.
Admission, Cancellation, And Execution
PrepareExecution never reserves backend admission or an active-generation
permit. Its context governs preparation only; cancellation after it returns
does not invalidate the handle.
RunPrepared uses its own context for credential revalidation, backend
admission, active-generation waiting, model generation, output validation, and
any internal repair. Admission occurs when RunPrepared begins so a consumer
cannot occupy bounded capacity while inspecting or persisting preparation
details.
For a limited backend, the admission lease covers the complete prepared execution attempt after admission: generation, validation, internal repair, and every success or failure exit. Actual generation continues to use the backend's FIFO active-generation permit. Existing capacity error identity, cancellation behavior, and release guarantees remain in force.
Because a prepared handle is one-attempt, cancellation or capacity rejection does not make it reusable. Retry and backoff policy remains with the consumer, which may create a new prepared handle when another attempt is appropriate.
Results And Failures
On success, RunPrepared returns the existing caller-owned RunResult. Its
source-derived provenance must match Details, including prompt identity and
hash, rendered-prompt hash, session ID, selected profile and backend,
effective target, and input hashes.
RunResult.StartTime, EndTime, and Duration describe the
RunPrepared execution attempt. They exclude preparation time and any delay
while the consumer retained the handle. Preparation timing remains in
PreparedRun.
Preparation failure returns no handle. After successful preparation,
RunPrepared retains the existing rule that an operational failure returns no
partial RunResult; the consumer already has independent preparation details.
A completed content-validation failure remains a successful result with
ValidationFailed.
PrepareExecution preserves the public error categories of Prepare.
RunPrepared preserves applicable invalid-request, credential, capacity,
generation, validation, collaborator, and cancellation identities without
reintroducing source-loading or rendering failures from frozen state.
Compatibility And Existing Workflows
This feature is additive:
Prepareremains the simple preparation-only operation;Runremains the simple prepare-and-execute operation with its current early-admission and error-ordering behavior;PreparedRunandRunResultretain their existing stable JSON representations;- model-client and artifact-reader extension interfaces remain unchanged; and
- backend routing, concurrency limits, queue capacities, and provider wire behavior remain unchanged.
The new workflow may share internal machinery with Prepare and Run, but it
must not change their observable behavior merely to simplify implementation.
Documentation
The completed documentation set has these ownership boundaries:
- exported declarations and GoDoc own the exact handle, method, lifecycle, ownership, concurrency, credential, error, and cancellation contracts;
- the promptkit consumer guide explains when to use
Prepare,Run, or the two-phase prepared-execution workflow; and - internal runner, source-validation, capacity, and model-client documentation describe the implemented collaborator boundaries without duplicating public contracts.
No release document is part of the feature implementation itself. Release guidance is prepared only when the resulting public API is selected for publication.
Non-Goals
This work does not include:
- serializable, durable, resumable, or cross-process execution handles;
- reuse for multiple consumer-initiated executions;
- concurrent execution of one handle;
- capacity reservation during preparation;
- a background task queue, priorities, worker lifecycle, or job status;
- retry, backoff, or provider failover policy;
- a separate
RunDetailedconvenience method; - adding preparation details or rendered messages to every
RunResult; - prompt or profile inspection APIs;
- structured capacity or generation errors;
- freezing environment-variable credential values for the handle lifetime;
- snapshotting provider state or mutable behavior inside an injected
LLMClient; - allowing target, validation, session, variable, or input overrides after preparation; or
- changing existing
Prepare,Run, file-format, provider-request, or stable JSON contracts.
Target End State
After this work:
- consumers can perform and persist preflight before starting provider work;
- one prepared handle executes exactly the source-derived prompt, target, schema, inputs, session, and messages described by its public details;
- execution never reloads or rerenders consumer sources;
- direct credentials remain confined to opaque, explicitly discardable state;
- environment credentials are not retained across the preflight boundary;
- backend capacity is reserved only when execution begins;
- one handle can start at most one execution attempt, including any internal repair calls owned by that attempt;
- preparation details remain available after execution success or failure;
- existing simple
PrepareandRunconsumers remain unaffected; and - Promptkit continues to own reusable execution mechanics without taking on downstream persistence, redaction, retry, or job-management policy.