Add a roadmap, implementation plan, and built-in profile defaults for a production-ready public library package

This commit is contained in:
2026-07-04 11:38:14 -05:00
parent 1d7fac0a47
commit 89cafcefec
26 changed files with 805 additions and 308 deletions

View File

@@ -1,134 +1,176 @@
# Library API Roadmap
# Library API Production Roadmap
This roadmap defines the target behavior for making Scriptorium usable as an imported Go library while retaining the current standalone CLI and HTTP application behavior.
This roadmap defines the target state for making Scriptorium's public Go package production-ready for downstream applications while preserving the existing CLI and HTTP behavior.
The implementation plan for this feature lives in `docs/roadmap/implementation.md`.
The library remains an additional adapter surface. It should not replace the subprocess, CLI, or HTTP contracts that already exist.
## Motivation
Scriptorium is currently optimized for subprocess use by other applications. That contract remains useful because it is language-neutral, operationally simple, and process-isolated.
Many Go applications can use Scriptorium more cleanly as an imported package than as a subprocess. A downstream developer should be able to keep prompt assets in standard Scriptorium format, pass application data through a small adapter, and receive a typed response without reimplementing prompt rendering, profile resolution, validation, or OpenAI-compatible request construction.
For Go callers, an imported library should provide:
The core consumer story is:
- typed requests and results instead of stdout/stderr parsing;
- direct `context.Context` cancellation;
- lower overhead for repeated calls;
- easier test integration through injected clients or fixtures;
- direct access to prepared-run data without process management;
- fewer integration points where secrets or output metadata can be mishandled.
- the downstream app owns one or more `prompt.yml` files in standard Scriptorium format;
- those prompts may use inline content, `content_file` references, variables, cache-control markers, and structured-output schemas;
- the app may provide its own `profile.yml`, or select a standard built-in/profile-template configuration;
- the app supplies an API key as a normal Go value when the selected profile requires one;
- the app calls the public Go package to prepare or run the request and receives typed results.
The library is an additional adapter surface, not a replacement for the CLI or HTTP API.
## Current State
The public package already provides the first library facade:
- root package import;
- typed engine construction;
- typed prepare/run requests and results;
- file and inline artifact references;
- execution overrides;
- custom LLM injection for testing or alternate execution;
- public error categories that map internal failures to stable caller-facing errors.
The remaining production-readiness gaps are mostly about consumer ergonomics and asset sourcing:
- callers are still oriented around filesystem prompt/profile/schema directories;
- embedded prompt/profile/schema assets are not a first-class public use case;
- standard or built-in profile selection is not yet available;
- library credential supply is still tightly coupled to environment-variable lookup rather than direct API-key values;
- public documentation and examples need to show the intended downstream app adapter pattern.
## Target State
Scriptorium should expose a small public Go API suitable for common embedding use cases:
The public package should let a downstream Go application use standard Scriptorium assets without temporary directories, subprocess invocation, or internal package imports.
- construct an engine from app-level settings such as prompt, profile, and schema directories;
- prepare a prompt request without calling an LLM;
- run a prompt request and receive a typed result;
- pass file and inline artifacts;
- apply profile selection, runtime overrides, vars, validation behavior, cache-control behavior, and structured-output behavior consistently with CLI/HTTP;
- inject a custom LLM client or HTTP client where needed;
- preserve existing CLI and HTTP behavior by continuing to route all entry paths through the same use-case layer.
### Prompt Assets
The public library API should be stable, narrow, and intentionally higher-level than the current `internal/*` package layout.
The library should support prompt definitions from:
## Public Package Policy
- existing prompt directories;
- a single prompt file;
- `fs.FS`, including `embed.FS`.
The public package should be the module root:
Prompt syntax should remain the standard Scriptorium prompt YAML format. `content_file` references should continue to be supported and should resolve relative to the prompt definition's source location within the same asset source.
```go
import "gitea.maximumdirect.net/eric/scriptorium"
```
The public API should not introduce a separate in-code prompt DSL as the primary path. YAML remains the canonical authoring format so prompts can be shared between CLI, HTTP, subprocess, and library usage.
Recommended usage shape:
### Schema Assets
```go
engine, err := scriptorium.NewEngine(scriptorium.Config{
PromptDir: "./prompts",
ProfileDir: "./profiles",
SchemaDir: "./schemas",
})
if err != nil {
return err
}
Structured-output schemas should be loadable from the same kinds of sources as prompt definitions:
prepared, err := engine.Prepare(ctx, scriptorium.RunRequest{
PromptID: "generic.markdown_summary",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File("./transcript.md"),
},
})
- existing schema directories;
- a single schema file where appropriate;
- `fs.FS`, including `embed.FS`.
result, err := engine.Run(ctx, scriptorium.RunRequest{
PromptID: "generic.markdown_summary",
Inputs: map[string]scriptorium.ArtifactRef{
"transcript": scriptorium.File("./transcript.md"),
},
})
```
Schema references should retain the existing prompt-format semantics. A schema referenced by a prompt should resolve through the configured schema source, not through ad hoc caller code.
## Policy Decisions
### Profile Assets
### Public Package Scope
The library should support both custom and standard profile configuration:
Expose a narrow root facade package and keep existing `internal/*` packages internal.
- existing profile directories;
- a single profile file;
- `fs.FS`, including `embed.FS`;
- direct public profile values for applications that already have profile configuration in memory;
- built-in/profile-template helpers for common OpenAI-compatible targets.
Reasoning:
Custom profiles and built-in/template profiles should flow through the same internal profile resolution and request-construction path. The built-in path should not become a separate execution mode.
This gives callers the workflow they need without freezing the internal architecture as public API. It also preserves the current package-boundary policy and keeps future refactoring possible.
### Built-In Profile Templates
### Public Type Strategy
Built-in support should favor stable profile templates over a large registry of fixed model IDs.
Define public facade types and map them to internal domain types.
For example, the public package should make it easy to construct or select an OpenAI-compatible profile by supplying the durable parts of the profile:
Reasoning:
- profile ID or name;
- base URL;
- model;
- whether the profile requires an API key;
- default numeric parameters where desired;
- structured-output and extra-parameter behavior consistent with normal profiles.
Public types can be designed around caller needs and long-term stability. Internal types can continue to evolve with implementation details such as adapter metadata, validation internals, and provider-specific behavior.
The package may include a small set of named helpers for common OpenAI-compatible services, but those helpers should avoid hard-coding a broad and fast-changing list of model names.
### CLI And HTTP Reuse
### Credentials
Keep CLI and HTTP on current internal wiring for the initial library release. Consider migrating them to the public facade only after the facade proves stable.
The public package must keep raw API keys out of prompt/profile YAML, prepared-run output, run results, logs, and examples.
Reasoning:
For the public library API, the single supported credential-supply method should be a direct API-key value passed by the consuming Go application. The consuming application is responsible for loading and managing its own secrets before calling Scriptorium.
This minimizes risk to the existing subprocess and HTTP contracts while adding the new API. It also avoids forcing the first public facade to satisfy every adapter edge case immediately.
This may be exposed as a field such as `Config.APIKey`, an option such as `WithAPIKey`, or an equivalent request/engine-level value that is easy to pass through an application adapter. The exact API should avoid accidental serialization in prepared output, run results, logs, and examples.
### Error Surface
The public package should not encourage raw API-key storage in prompt/profile YAML. Existing CLI behavior may continue to use environment-variable references for compatibility, but the production library path should not introduce a separate credential resolver or secret-manager abstraction.
Expose public sentinel errors or typed error categories and map internal errors to them while preserving wrapped context.
### Public API Shape
Reasoning:
The public API should remain narrow, idiomatic, and stable. Recommended additions include:
Library callers need stable, idiomatic error checks. Mapping internal errors avoids exposing internal package paths as public compatibility promises.
- engine options for prompt/profile/schema directories;
- engine options for prompt/profile/schema `fs.FS` sources;
- engine options for single prompt/profile/schema files where useful;
- public profile/template constructors that map to internal profile definitions;
- a direct API-key value for profiles that require authentication;
- examples showing `embed.FS`, custom profile files, template profile selection, and fake LLM testing.
The public package should continue to expose facade types rather than exporting internal package types. Internal package layout should remain free to evolve.
## Scope
In scope:
- Public facade package for library consumers.
- Public request, result, prepared-run, artifact reference, execution override, validation, and config types.
- Public constructors for common file and inline input references.
- Public engine methods for `Prepare` and `Run`.
- Optional dependency injection for LLM behavior and HTTP behavior.
- Stable error behavior suitable for `errors.Is` and `errors.As`.
- Tests proving public API behavior matches CLI/use-case behavior.
- Documentation and examples for library usage after implementation.
- first-class `fs.FS` support for public library prompt, profile, and schema sources;
- ergonomic single-file asset options where they reduce caller boilerplate;
- built-in/profile-template helpers for common OpenAI-compatible usage;
- in-memory public profile values where appropriate;
- direct API-key value support for the public library path;
- consumer-facing examples under `examples/`;
- consumer package documentation under `docs/consumers/` once behavior is implemented;
- tests proving library behavior matches existing CLI/use-case behavior.
Out of scope for the first library release:
Out of scope:
- Making every `internal/*` package public.
- Replacing or rewiring the CLI or HTTP adapters.
- Adding a durable run store or workflow engine.
- Adding broad provider-specific SDK surfaces.
- Adding non-Go language bindings.
- Adding global mutable configuration.
- changing standard prompt, profile, or schema file formats;
- exposing internal packages as public API;
- replacing or removing CLI, HTTP, or subprocess support;
- adding a multi-step workflow engine;
- adding non-Go bindings;
- maintaining a comprehensive provider/model catalog;
- accepting raw API keys in serialized YAML/JSON configuration;
- adding a credential resolver or secret-manager abstraction.
## Acceptance Criteria
- A Go caller can import the root module and run a prompt without invoking a subprocess.
- A Go caller can prepare a prompt without invoking an LLM.
- Public library behavior matches current CLI/HTTP use-case semantics for prompt/profile loading, artifact reading, rendering, validation, and model invocation.
- A Go caller can import the root package and run a standard Scriptorium prompt without invoking a subprocess.
- A Go caller can use prompt definitions from `embed.FS`, including prompts with `content_file` references.
- A Go caller can use structured-output schemas from `embed.FS` or filesystem sources.
- A Go caller can provide a custom profile from filesystem, `fs.FS`, or public in-memory profile values.
- A Go caller can select a standard OpenAI-compatible profile template without writing a full profile file.
- A Go caller can supply an API key as a normal Go value without raw secrets appearing in serialized config, prepared output, or results.
- Public library behavior remains consistent with CLI/HTTP semantics for rendering, validation, profile resolution, runtime overrides, structured output, cache control, and LLM invocation.
- Existing CLI and HTTP behavior remains unchanged.
- Library tests use injected/fake LLM behavior and do not require real provider credentials.
- Public documentation is concise and limited to implemented behavior once code exists.
- Library tests use fake or local LLM boundaries and do not require real provider credentials.
- Public docs outside `docs/roadmap/` describe only implemented behavior after the feature is built.
## Design Decisions
### Asset Source API
Add first-class `fs.FS` options for prompt, profile, and schema sources while keeping existing directory-based configuration. Also add single-file convenience options where they remove meaningful caller boilerplate. Resolve `content_file` references relative to the prompt file's location inside the same source.
Reasoning:
This is the most idiomatic path for production Go libraries because it supports `embed.FS`, `os.DirFS`, tests, and in-memory fixture files through the same abstraction. It also avoids requiring downstream applications to unpack embedded assets into temporary directories.
### Built-In Profile Strategy
Provide stable profile-template helpers for OpenAI-compatible endpoints rather than a broad registry of fixed provider/model profiles. Let callers choose the model and endpoint where those values are service-specific or fast-changing.
Reasoning:
Endpoint shape and credential mechanics are relatively stable; model catalogs change frequently. Templates give consumers a short, correct path without making Scriptorium responsible for tracking every provider's model list.
### In-Memory Profile Values
Expose a small public profile facade type for in-memory profile configuration and map it to internal profile definitions. Keep it intentionally aligned with the existing profile YAML contract.
Reasoning:
Many applications already hold configuration in typed structs and should not need to generate YAML files just to call Scriptorium. A public facade keeps internal types private while making the library practical for production use.