9.5 KiB
Library API Implementation Plan
This plan implements the target state in docs/roadmap/library.md.
Audience: LLM coding agents implementing the feature in order. Follow docs/policy/architecture.md, docs/policy/development.md, and docs/policy/documentation.md before changing code.
Constraints
- Add a public root package named
scriptorium; keep existinginternal/*packages internal. - Define public facade types and convert to/from internal domain types. Do not alias internal domain types as the public API.
- Do not rewire CLI or HTTP through the public facade in this implementation.
- Preserve current CLI, HTTP, prompt/profile loading, validation, secret-handling, and outbound LLM behavior.
- Do not add dependencies.
- Keep each stage passing
go test ./...before moving to the next stage.
Stage 1: Public Types, Engine Construction, And Prepare
Goal: make prompt preparation usable from an imported root package without calling an LLM.
Public Package
Create Go files at the module root using:
package scriptorium
Expose:
type Engine struct { /* unexported fields */ }
type Config struct {
PromptDir string
ProfileDir string
SchemaDir string
Timeout time.Duration
HTTPClient *http.Client
}
type Option func(*engineOptions) error
func NewEngine(cfg Config, opts ...Option) (*Engine, error)
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error)
Construction rules:
PromptDirandProfileDirare required.SchemaDirdefaults to the same built-in default used by app config.Timeout, when non-zero, configures the default OpenAI-compatible client timeout.HTTPClient, when non-nil, is used by the default OpenAI-compatible client.NewEnginewires the same internal components used by CLI/HTTP: filesystem prompt/profile repositories, composite artifact reader, Go template renderer, standard validator, and OpenAI-compatible LLM client.- Return public
ErrInvalidConfigfor invalid engine configuration.
Public Types
Define public facade types with exported fields:
RunRequestPreparedRunArtifactRefArtifactExecutionTargetExecutionTargetOverrideExecutionTargetPresenceOutputContractValidationResultTokenUsageRenderedMessageCacheControlStructuredOutputSpec
Use the same enum string values as internal domain types for formats, validation modes, validation statuses, artifact ref types, cache-control type, and structured-output type.
Required request shape:
type RunRequest struct {
PromptID string
PromptVersion string
ProfileID string
Inputs map[string]ArtifactRef
Vars map[string]string
Execution *ExecutionTargetOverride
Validation *OutputContract
Metadata map[string]string
}
ExecutionTargetOverride must preserve numeric override presence using pointer fields:
Temperature *float64
MaxTokens *int
TopP *float64
TimeoutSeconds *int
PreparedRun should include the same user-observable fields as internal domain.PreparedRun, but should not expose internal-only target presence metadata.
Input Helpers
Expose:
func File(path string) ArtifactRef
func Inline(body string) ArtifactRef
func InlineWithURI(uri string, body string) ArtifactRef
Mapping:
File(path)maps to artifact typefilewithURI: path.Inline(body)maps to artifact typeinlinewithBody: body.InlineWithURI(uri, body)maps to artifact typeinlinewith both fields set.
Conversion Layer
Implement unexported conversion helpers in the public package:
- public run request to internal
domain.RunRequest - internal
domain.PreparedRunto publicPreparedRun - internal artifacts/messages/contracts/validation/usage/structured-output to public equivalents
- public execution override to internal
domain.ExecutionTargetOverride
Conversions must deep-copy maps and slices that cross the public/internal boundary.
Tests
Add root package tests.
Required tests:
NewEnginerejects missingPromptDir.NewEnginerejects missingProfileDir.Prepareworks withexamples/config.ymldirectories when passed directly throughConfig.Prepareworks withFileinput refs.Prepareworks withInlineinput refs.Prepareoutput does not expose raw API-key values or internal target presence metadata in JSON.- Explicit zero execution overrides survive into prepared effective settings.
Verification
Run:
go test ./...
Stage 2: Run, LLM Injection, And Public Errors
Goal: make full execution usable and testable without real provider credentials.
Public Run Method
Expose:
func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error)
RunResult should expose:
- run ID
- artifact
- raw output
- validation result
- prompt/profile/model metadata
- effective model params
- input hashes
- token/cache usage
- start/end/duration timing
Do not expose raw API-key values.
Public LLM Injection
Expose:
type LLMClient interface {
Generate(context.Context, GenerateRequest) (*GenerateResponse, error)
}
func WithLLMClient(client LLMClient) Option
Public GenerateRequest must include:
- rendered prompt
- effective execution target
- execution target presence
- structured-output spec
Public GenerateResponse must include:
- content
- token usage
Implementation rule:
WithLLMClientwraps the public client in an unexported adapter that satisfiesinternal/llm.Client.- The adapter converts internal generate requests to public generate requests and converts public generate responses back to internal responses.
- A nil client passed to
WithLLMClientreturnsErrInvalidConfig.
Default behavior:
- If no custom LLM client is supplied,
NewEngineusesinternal/llm.NewOpenAICompatibleClient. Config.TimeoutandConfig.HTTPClientapply only to the default OpenAI-compatible client.
Public Errors
Define public sentinel errors:
ErrInvalidConfigErrInvalidRequestErrPromptNotFoundErrProfileNotFoundErrPromptLoadErrProfileLoadErrArtifactLoadErrPromptRenderErrLLMGenerateErrValidation
Public methods must map internal errors to public sentinels while preserving wrapped context. Callers must be able to use errors.Is.
Mapping rules:
- missing/invalid public engine config ->
ErrInvalidConfig - internal
usecase.ErrInvalidRequest->ErrInvalidRequest - internal prompt not found ->
ErrPromptNotFound - internal profile not found ->
ErrProfileNotFound - internal prompt load errors ->
ErrPromptLoad - internal profile load errors ->
ErrProfileLoad - internal artifact load errors ->
ErrArtifactLoad - internal prompt render errors ->
ErrPromptRender - internal LLM generate errors ->
ErrLLMGenerate - internal validation runtime errors ->
ErrValidation
Do not expose internal sentinel values as public API.
Tests
Required tests:
Runsucceeds withWithLLMClientfake and returns typed artifact, raw output, validation, metadata, and usage.Runpasses rendered prompt, effective execution target, and target presence to the injected LLM client.Runvalidation failure returns a successful result with failed validation, not an error.- public errors support
errors.Isfor invalid request, prompt not found, profile not found, artifact load, render failure, LLM failure, and validation runtime failure. - nil
WithLLMClient(nil)returnsErrInvalidConfig. - default OpenAI-compatible client can still be constructed without real provider credentials.
Verification
Run:
go test ./...
Stage 3: Public Documentation And Consumer Examples
Goal: document implemented library behavior in canonical public-consumer docs.
Docs
After Stages 1 and 2 are implemented, update:
README.md: add a short link to library usage without turning the README into a manual.docs/internal/adapters.md: list the public library facade as an implemented adapter surface.docs/consumers/api.md: describe the public consumer API at a high level.docs/consumers/pkg-scriptorium.md: document the root package usage, types, errors, and examples.
Create docs/consumers/ if it does not exist.
Do not document unimplemented future library features outside docs/roadmap/.
Examples
Add copyable library examples only if they can be tested without real credentials.
Recommended example:
examples/go-library/prepare/main.goor equivalent prepare-only example usingexamples/prompt/profile/fixture assets.
If adding a run example, it must use an injected fake LLM client and must not require provider credentials.
Tests
Required tests:
- doc/example smoke coverage for any added Go example using
go testorgo test ./.... - existing CLI/HTTP tests continue to pass unchanged.
Verification
Run:
go test ./...
go run ./cmd/scriptorium render \
--config ./examples/config.yml \
--prompt generic.markdown_summary \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--format json
Final Checks
Before considering the feature complete:
- Confirm the root package can be imported as
gitea.maximumdirect.net/eric/scriptorium. - Confirm public package tests do not require real provider credentials.
- Confirm
go test ./...passes. - Confirm the render smoke command passes.
- Confirm non-roadmap docs describe only implemented behavior.
- Confirm no public result or rendered/prepared output exposes raw API-key values.
- Confirm
git diffdoes not include unrelated CLI/HTTP behavior changes.