26 KiB
Package promptkit
Purpose
This guide helps Go consumers assemble Promptkit and choose the main preparation or execution workflow. The declarations and GoDoc in the root package own exact field, option, serialization, concurrency, ownership, failure, and cancellation semantics. The framework format reference owns prompt, profile, and schema file contracts.
Import the package as:
import "gitea.maximumdirect.net/eric/promptkit"
The following Go fragments are illustrative and omit surrounding package, import, and error-handling code. Use the maintained examples for complete programs.
Construct An Engine
Create an engine with
NewEngine. A directory-backed setup supplies a prompt
directory and may supply profile and schema directories:
engine, err := promptkit.NewEngine(promptkit.Config{
PromptDir: "prompts",
ProfileDir: "profiles",
SchemaDir: "schemas",
})
Options support single-file or fs.FS sources, in-memory profiles,
engine-scoped backends, and injected artifact or model clients. Consult the
constructor and option GoDoc for composition, precedence,
validation, and default transport behavior. Source discovery, format
validation, and profile precedence are defined by the
framework format reference.
Supply Embedded Application Defaults
Use WithFallbackProfileFS when an application packages profile definitions
that should apply unless an operator provides an ordinary configured profile
with the same ID. For example, an application can embed its defaults while
continuing to use ProfileDir for operator overrides:
//go:embed profiles/*.yaml
var applicationProfiles embed.FS
engine, err := promptkit.NewEngine(promptkit.Config{
PromptDir: "prompts",
ProfileDir: operatorProfileDir,
},
promptkit.WithFallbackProfileFS(applicationProfiles, "profiles"),
)
Keep application-owned profile IDs and definitions in the embedded source.
Use the ordinary configured profile source for operator overrides. Leave
operatorProfileDir empty when the operator did not configure an override
directory; a non-empty path names an authoritative higher-precedence source,
so an unavailable or unreadable directory is an error rather than a reason to
fall back. The
framework format reference
owns the exact profile format and lookup order; the
WithFallbackProfileFS GoDoc owns its option contract and
validation rules.
Inspect A Prompt Before Preparation
Use Engine.InspectPrompt to check one configured prompt's
declared inputs and output workflow without creating placeholder inputs or
resolving a profile:
inspection, err := engine.InspectPrompt(ctx, "meeting.summary", "")
if err != nil {
return err
}
for _, input := range inspection.Inputs {
// Compare the declared input with application configuration.
}
Use this configuration-time boundary when the application needs only the
declared prompt interface. Use InspectProfile separately when it must also
check a configured profile. Use Prepare when it needs inputs, schemas, or
rendered messages, and use prepared execution when that work must remain tied
to later execution. The method's GoDoc owns exact fields,
hash, ownership, and error semantics.
Prepare Without Model Execution
Engine.Prepare resolves the selected prompt and profile,
loads inputs and any structured-output schema, and renders messages without
calling a model client. Choose it when the prepared value is the final
inspection or persistence result and no later execution must be tied to that
exact snapshot:
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{
PromptID: "meeting.summary",
Inputs: map[string]promptkit.ArtifactRef{
"note": promptkit.Inline("Synthetic meeting notes"),
},
})
The maintained
offline preparation example
shows a complete runnable setup with a prompt file, in-memory profile, and
inline input. Exact request requirements and prepared-result fields belong to
the RunRequest and PreparedRun GoDoc.
Prepare Now And Execute The Same Snapshot Later
Use Engine.PrepareExecution when an application must
inspect or persist preflight details before deciding whether to start model
work, while ensuring that later execution uses those exact rendered messages,
inputs, target settings, and validation resources:
preparedExecution, err := engine.PrepareExecution(ctx, promptkit.RunRequest{
PromptID: "meeting.summary",
Inputs: map[string]promptkit.ArtifactRef{
"note": promptkit.Inline("Synthetic meeting notes"),
},
})
if err != nil {
return err
}
defer preparedExecution.Discard()
details := preparedExecution.Details()
// Inspect or persist an application-selected safe subset of details.
result, err := engine.RunPrepared(ctx, preparedExecution)
Preparation does not call the model or reserve backend capacity.
RunPrepared executes from the retained snapshot rather than reloading
consumer sources. The handle is opaque in-process state, while Details
contains rendered content and remains subject to the application's data
handling policy. The
PreparedExecution and method GoDoc and
engine operation GoDoc own exact lifecycle, engine-binding,
credential, cancellation, timing, and error semantics.
If the request supplied Observer, successful preparation retains that
function for RunPrepared; preparation itself emits nothing. Observation
starts only when the owning engine successfully claims the handle. Reused,
discarded, foreign, and invalid handles emit nothing. Discard clears the
retained callback without invoking it. The function's closure is retained by
identity, so synchronize any shared state it uses. Prepared start events can
include identities already resolved in the frozen snapshot.
Execute And Validate
Engine.Run performs the same preparation, invokes the
configured model client, classifies the generated artifact, and validates the
content in one call. Choose it when the application does not need a preflight
boundary tied to the eventual execution. A completed content check may return
ValidationFailed in the result; an operational inability to validate returns
an error.
The maintained
offline execution example injects a
deterministic model client and exercises Run without credentials, network
access, or paid calls. It is intentionally separate from the preparation
example so each workflow and its small prompt fixture can be copied and run on
its own.
Use the RunResult and ValidationResult GoDoc for the
returned data and the Engine.Run GoDoc for failure and cancellation
semantics. The
OpenAI-compatible integration contract
owns the built-in client's outbound HTTP behavior.
Observe An Ordinary Run
Set RunRequest.Observer to receive synchronous metadata events from Run:
request.Observer = func(event promptkit.ExecutionEvent) {
// Translate selected metadata into an application-owned record.
}
result, err := engine.Run(ctx, request)
An ordinary run emits started before request conversion, followed by any
attempt and retry-delay completions, then one finished event on a normal
return. Prepare and PrepareExecution emit no events. A start event has no
resolved prompt, profile, or model; later events gain identities only as
selection succeeds. The terminal event has dispatched-attempt, transient-retry,
repair-call, duration, and presence-aware usage totals. Its usage is an
aggregate, not an additional billable attempt. A known zero differs from an
unknown count, and incomplete totals must not be treated as complete cost
accounting. A failed operation still returns no partial RunResult.
An injected client that leaves UsagePresence nil can report known nonzero
counts, but its zero counts remain unknown to observations; supply explicit
presence when measured zeros matter. Existing RunResult.Usage sums returned
values independently of observation presence.
Callbacks run synchronously, in order for one operation, and can overlap for different operations. Synchronize shared callback state and avoid blocking on work that needs the same run's admission slot. Callback panics propagate; there is no durable event history or promise of a terminal event after panic. Events omit content and error objects, but selected identifiers may still be sensitive. See the observation GoDoc for exact fields. The offline proposal and assessment example shows application-owned event collection and a separate semantic acceptance decision using the built-in HTTP path without network access.
Repair A Structured Result
Set a small additional-call budget when a structurally invalid result can be corrected automatically:
request.Validation = &promptkit.OutputContract{
Format: promptkit.FormatJSON,
ValidationMode: promptkit.ValidationJSONSchema,
SchemaPath: "events.schema.json",
RepairAttempts: 1,
}
Each repair attempt is another model generation, so it can increase latency and
usage; RunResult.Usage is cumulative and Validation.RepairAttempts reports
repair generations rather than transient HTTP attempts. Exhaustion still
returns the final failed validation result. basic validation can also repair
an empty candidate, but structural
validity is not evidence of factual or domain correctness. See the
output-contract format reference and
OutputContract GoDoc for the exact budget and eligibility
rules.
The built-in client also retries eligible temporary HTTP statuses. By default,
each generation can make two additional attempts; a repair generation receives
its own budget. Set Config.TransientRetries to a pointer to zero for the
previous single-attempt behavior, or set RunRequest.TransientRetries to
override one execution. A custom HTTP transport still uses this policy;
injected LLMClient implementations retain their own retry policy. Application
semantic retries are separate runs and can multiply provider traffic further.
The caller context covers capacity and retry waiting as well as attempts;
without a deadline a valid provider delay can be long. Promptkit does not
infer exactly-once or complete cost accounting from a retry outcome.
See the provider integration contract
for eligible statuses, waits, timeouts, and provider timing.
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:
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
can select a profile explicitly or use the prompt's default profile, and can
replace execution settings or the complete output contract.
The public value GoDoc defines nil, empty, zero, replacement, copy, and credential behavior. The framework format reference defines how those request values interact with prompt definitions, file-backed and application fallback profiles, built-ins, schemas, and framework defaults.
For programmatic profiles,
OpenAICompatibleProfile converts ordinary
OpenAI-compatible settings into a value accepted by WithProfiles.
Choose A Profile Ownership Model
Choose the profile source that matches the application's continuity needs:
- Select
promptkit-openrouter-standardorpromptkit-openrouter-autowhen the maintained literal name is the desired consumer contract. Their current selections are in the format reference. - Select an external catalog profile directly when the application accepts that profile's catalog volatility. The selected external inventories are linked from the format reference.
- Define a complete application-owned profile with an explicit backend or endpoint, model, and settings when the application owns those choices.
An application profile that inherits an external base follows later changes to that base and fails if the base disappears; it is not a frozen copy. Promptkit tests the catalog versions selected in its module graph. Independently advancing a catalog dependency can remove an inherited base and produce an untested combination. Pinning the resolved module graph preserves the embedded data for a build, but cannot preserve provider availability. Running binaries neither download catalog updates nor automatically replace withdrawn models. See the release policy for the exact cross-version guarantees.
Alias A Built-In Profile
Give an application-owned profile ID a built-in base when prompts should select the application ID while inheriting the built-in target. The child can override only the setting it owns:
promptkit.WithProfiles(promptkit.Profile{
ID: "weather-light",
BaseProfileID: "promptkit-openrouter-standard",
ReasoningEffort: "high",
})
Select weather-light in a prompt or RunRequest.ProfileID; it remains the
reported selected profile. See the profile inheritance format
reference and the
Profile GoDoc for exact lookup, merging, and validation
behavior. If the base is an external profile, apply the volatility guidance
above when deciding whether to use an application-owned complete definition
instead.
Use The Rakestrawhome Built-In Profile
Set RAKESTRAWHOME_INFERENCE_API_KEY in the application environment, then
select rakestrawhome-gemma-4-31b as an ordinary profile ID. For example, a
prepared result identifies the selected built-in through
BackendRakestrawHome:
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{
PromptID: "meeting.summary",
ProfileID: "rakestrawhome-gemma-4-31b",
Inputs: inputs,
})
if err != nil {
return err
}
if prepared.SelectedBackendID != promptkit.BackendRakestrawHome {
return fmt.Errorf("unexpected backend %q", prepared.SelectedBackendID)
}
Do not register rakestrawhome manually. When adopting this built-in, remove
an existing WithBackend registration with that exact ID; retaining it causes
the intentional duplicate-ID configuration error. Direct request credentials
and runtime endpoint overrides remain supported under their ordinary GoDoc and
format contracts.
Inspect A Profile Before Prompt Work
Use Engine.InspectProfile to validate one configured
profile without constructing a synthetic prompt or placeholder inputs. It
resolves the profile's effective target but does not prepare or execute a
prompt:
inspection, err := engine.InspectProfile(ctx, profileID)
if err != nil {
return err
}
target := inspection.EffectiveModelParams
if target.APIKeyEnv != "" {
// This is a configured optional environment lookup source.
} else if inspection.APIKeyRequired {
// Arrange a direct credential before later execution.
}
Use this configuration-time boundary when only the profile and its target need
checking. Use Prepare when the application also needs prompt, input, schema,
or rendering work; use prepared execution when that work must remain tied to a
later execution. A reported APIKeyEnv is a configured optional source, while
APIKeyRequired is the explicit local requirement. The
credential format reference and the method's
GoDoc own the exact precedence, timing, result, and error
contracts.
Set A Per-Run Session And Reasoning
Supply a direct session ID when one prompt should be correlated with a consumer-managed conversation or workflow without changing prompt variables:
reasoning := "high"
result, err := engine.Run(ctx, promptkit.RunRequest{
PromptID: "meeting.summary",
SessionID: "conversation-42",
Inputs: map[string]promptkit.ArtifactRef{
"note": promptkit.Inline("Synthetic meeting notes"),
},
Execution: &promptkit.ExecutionTargetOverride{
ReasoningEffort: &reasoning,
},
})
A nil reasoning pointer inherits the selected profile, a pointer to a
nonblank string replaces it, and a pointer to a blank string disables
reasoning for that run. Session IDs are correlation metadata, not credentials;
use stable, non-secret values that are safe to expose to collaborators and
providers. The
RunRequest and ExecutionTargetOverride GoDoc owns the
exact normalization, precedence, error, copying, and exposure contract.
Configure A Local OpenAI-Compatible Endpoint
Choose the smallest configuration that fits how the endpoint will be reused.
Use An Endpoint-Only Profile
Put the endpoint directly on an in-memory profile when only that profile needs it and shared backend identity or capacity policy is unnecessary:
engine, err := promptkit.NewEngine(promptkit.Config{
PromptDir: "prompts",
},
promptkit.WithProfiles(promptkit.Profile{
ID: "local-summary",
Endpoint: "http://localhost:8000/v1",
Model: "example-model",
}),
)
Endpoint-only profiles have an empty backend ID and remain unrestricted by backend capacity policy.
Use The Conventional Local Backend
Use LocalBackend when profiles should share the conventional local
identity, endpoint, and concurrency limit:
engine, err := promptkit.NewEngine(promptkit.Config{
PromptDir: "prompts",
},
promptkit.WithBackend(
promptkit.LocalBackend("http://localhost:8000/v1", 2),
),
promptkit.WithProfiles(promptkit.Profile{
ID: "local-summary",
BackendID: promptkit.BackendLocal,
Model: "example-model",
}),
)
The helper is explicit: it does not pre-register a backend or read environment
variables. Supplying a positive limit leaves queue capacity omitted, so normal
backend registration selects the existing default waiting capacity of 1024.
The returned value still enters the engine through WithBackend.
Configure A Complete Backend
Use a keyed Backend value for authentication, extra request parameters, an
explicit queue capacity, a custom ID, or multiple local endpoints:
noWaiting := 0
engine, err := promptkit.NewEngine(promptkit.Config{
PromptDir: "prompts",
},
promptkit.WithBackend(promptkit.Backend{
ID: "local-gpu",
Endpoint: "http://gpu-host:8000/v1",
APIKeyEnv: "LOCAL_GPU_API_KEY",
ExtraParams: map[string]any{"provider_option": "enabled"},
ConcurrencyLimit: 2,
QueueCapacity: &noWaiting,
}),
promptkit.WithProfiles(promptkit.Profile{
ID: "gpu-summary",
BackendID: "local-gpu",
Model: "example-model",
}),
)
Use distinct custom IDs when registering multiple local endpoints.
Registrations belong to one engine and custom IDs cannot replace built-ins.
The Backend, LocalBackend, and WithBackend GoDoc
defines exact construction, validation, copying, uniqueness, concurrency, and
request-default behavior.
Both file-backed and in-memory profiles select a registration through
backend or Profile.BackendID. Profile and request endpoint overrides retain
that routing and capacity identity. PreparedRun.SelectedBackendID,
RunResult.SelectedBackendID, and the effective ExecutionTarget.BackendID
expose it to consumers and injected model clients. Endpoint-only profiles
remain supported and expose an empty backend ID.
Limit Backend Concurrency
Set Backend.ConcurrencyLimit when a backend needs protection from too many
simultaneous model calls. Leaving QueueCapacity nil, as in the local-backend
example above, selects the default waiting capacity of 1024.
To accept no waiting backlog beyond the active calls, provide an explicit zero:
noWaiting := 0
backend := promptkit.Backend{
ID: "local-gpu",
Endpoint: "http://gpu-host:8000/v1",
ConcurrencyLimit: 2,
QueueCapacity: &noWaiting,
}
The pointer distinguishes an explicit zero from omission. Keep using keyed
Backend literals so additive configuration fields remain source-compatible.
Capacity belongs to one engine and the selected backend ID; endpoint-only
profiles and custom backends without a configured limit remain unrestricted.
Exact validation, defaulting, ownership, and concurrency semantics belong to
the Backend GoDoc.
Credentials
File-backed profiles name an environment variable; in-memory profiles can
require a direct request key. Direct keys are request-scoped and are excluded
from supported JSON values and the package's String and GoString
summaries. The exact precedence and redaction guarantees belong to
RunRequest, GenerateRequest, and the profile GoDoc.
Protect Files And Generated Data
The default artifact reader opens a File reference as a caller-selected
operating-system path. It does not constrain paths to an application root,
impose an inbound request-size policy, or establish an untrusted-input security
boundary. Applications must validate and restrict untrusted paths and payloads
before constructing a request, or inject an artifact reader that enforces
their filesystem, authorization, and size policies.
Rendered messages, input and output artifact bodies, raw model output, and validation diagnostics can contain sensitive data. API-key redaction does not sanitize those values. Treat prepared values, results, collaborator requests, errors, and logs according to the application's data-access, retention, and secret-handling policies.
Extension Interfaces
Inject an LLMClient or ArtifactReader when the built-in
behavior does not fit the application. Their GoDoc defines concurrent use,
context handling, ownership of copied values, nil responses, and preservation
of collaborator errors. Implementations must honor cancellation, safely manage
copies they retain, avoid unsafe logging of content or credentials, and enforce
the application policy that motivated the injection.
Handle Errors
Use errors.Is with the
public error sentinels and operation GoDoc. The declarations
distinguish invalid construction, invalid requests, absent sources,
source-loading failures, collaborator failures, and operational validation
failures. Specific request conditions may also match the broader
ErrInvalidRequest, and injected collaborator identities are preserved where
documented. Invalid or duplicate backend registrations match
ErrInvalidConfig; selecting an unknown backend matches ErrProfileLoad.
When a limited backend has admitted all active and waiting calls, handle
ErrCapacityExceeded separately from request errors and provider failures:
result, err := engine.Run(ctx, request)
if errors.Is(err, promptkit.ErrCapacityExceeded) {
var capacityErr *promptkit.CapacityError
if errors.As(err, &capacityErr) {
// Record capacityErr.BackendID using application-owned diagnostics.
}
// Apply application policy: shed work, report overload, or retry later.
}
A rejected call returns no partial result and does not invoke the model
client. Promptkit does not prescribe retries or map this error to an HTTP
status; those choices remain with the consuming application. The
CapacityError GoDoc owns the exact typed-error
contract, while the Engine.Run and error GoDoc owns broad
error and cancellation identities.
For a non-2xx response from the built-in OpenAI-compatible client, inspect the status and deliberately selected provider diagnostic when useful:
var generationErr *promptkit.GenerationError
if errors.As(err, &generationErr) {
status := generationErr.StatusCode()
message := generationErr.ProviderMessage()
_, _ = status, message // Apply application presentation policy.
}
All provider fields are untrusted and can contain sensitive request or schema
fragments. Do not log, display, or return them without an application-specific
disclosure policy. Promptkit does not assign presentation behavior.
The GenerationError GoDoc owns the exact typed
error contract.
Generation failures also match ErrTransport when the built-in HTTP request
fails, or ErrMalformedResponse when a successful provider response cannot be
decoded or an injected client returns a nil successful response. Both retain
ErrLLMGenerate; transport failures retain their original causes for
errors.Is and errors.As. Caller cancellation and deadlines retain their
standard context identities. Arbitrary injected-client errors retain their own
identities without Promptkit guessing a failure category from their text.
An eligible built-in status that consumes a positive retry budget additionally
matches ErrRetriesExhausted; use errors.As to read
RetryExhaustedError.Attempts() and the final GenerationError status. With
explicit zero retries, the first eligible status is an ordinary generation
error, not exhaustion. Completed structural rejection remains a normal result
with failed validation, while an operational error returns no partial result.
Application Boundary
Promptkit is an importable library. It does not own a command, inbound HTTP API, process configuration, or deployment policy. Applications map the root package's results and errors into those concerns, including inbound size and trust policy.