322 lines
9.5 KiB
Markdown
322 lines
9.5 KiB
Markdown
# 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 existing `internal/*` 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:
|
|
|
|
```go
|
|
package scriptorium
|
|
```
|
|
|
|
Expose:
|
|
|
|
```go
|
|
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:
|
|
|
|
- `PromptDir` and `ProfileDir` are required.
|
|
- `SchemaDir` defaults 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.
|
|
- `NewEngine` wires 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 `ErrInvalidConfig` for invalid engine configuration.
|
|
|
|
### Public Types
|
|
|
|
Define public facade types with exported fields:
|
|
|
|
- `RunRequest`
|
|
- `PreparedRun`
|
|
- `ArtifactRef`
|
|
- `Artifact`
|
|
- `ExecutionTarget`
|
|
- `ExecutionTargetOverride`
|
|
- `ExecutionTargetPresence`
|
|
- `OutputContract`
|
|
- `ValidationResult`
|
|
- `TokenUsage`
|
|
- `RenderedMessage`
|
|
- `CacheControl`
|
|
- `StructuredOutputSpec`
|
|
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
func File(path string) ArtifactRef
|
|
func Inline(body string) ArtifactRef
|
|
func InlineWithURI(uri string, body string) ArtifactRef
|
|
```
|
|
|
|
Mapping:
|
|
|
|
- `File(path)` maps to artifact type `file` with `URI: path`.
|
|
- `Inline(body)` maps to artifact type `inline` with `Body: body`.
|
|
- `InlineWithURI(uri, body)` maps to artifact type `inline` with both fields set.
|
|
|
|
### Conversion Layer
|
|
|
|
Implement unexported conversion helpers in the public package:
|
|
|
|
- public run request to internal `domain.RunRequest`
|
|
- internal `domain.PreparedRun` to public `PreparedRun`
|
|
- 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:
|
|
|
|
- `NewEngine` rejects missing `PromptDir`.
|
|
- `NewEngine` rejects missing `ProfileDir`.
|
|
- `Prepare` works with `examples/config.yml` directories when passed directly through `Config`.
|
|
- `Prepare` works with `File` input refs.
|
|
- `Prepare` works with `Inline` input refs.
|
|
- `Prepare` output 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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
- `WithLLMClient` wraps the public client in an unexported adapter that satisfies `internal/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 `WithLLMClient` returns `ErrInvalidConfig`.
|
|
|
|
Default behavior:
|
|
|
|
- If no custom LLM client is supplied, `NewEngine` uses `internal/llm.NewOpenAICompatibleClient`.
|
|
- `Config.Timeout` and `Config.HTTPClient` apply only to the default OpenAI-compatible client.
|
|
|
|
### Public Errors
|
|
|
|
Define public sentinel errors:
|
|
|
|
- `ErrInvalidConfig`
|
|
- `ErrInvalidRequest`
|
|
- `ErrPromptNotFound`
|
|
- `ErrProfileNotFound`
|
|
- `ErrPromptLoad`
|
|
- `ErrProfileLoad`
|
|
- `ErrArtifactLoad`
|
|
- `ErrPromptRender`
|
|
- `ErrLLMGenerate`
|
|
- `ErrValidation`
|
|
|
|
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:
|
|
|
|
- `Run` succeeds with `WithLLMClient` fake and returns typed artifact, raw output, validation, metadata, and usage.
|
|
- `Run` passes rendered prompt, effective execution target, and target presence to the injected LLM client.
|
|
- `Run` validation failure returns a successful result with failed validation, not an error.
|
|
- public errors support `errors.Is` for invalid request, prompt not found, profile not found, artifact load, render failure, LLM failure, and validation runtime failure.
|
|
- nil `WithLLMClient(nil)` returns `ErrInvalidConfig`.
|
|
- default OpenAI-compatible client can still be constructed without real provider credentials.
|
|
|
|
### Verification
|
|
|
|
Run:
|
|
|
|
```bash
|
|
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.go` or equivalent prepare-only example using `examples/` 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 test` or `go test ./...`.
|
|
- existing CLI/HTTP tests continue to pass unchanged.
|
|
|
|
### Verification
|
|
|
|
Run:
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. Confirm the root package can be imported as `gitea.maximumdirect.net/eric/scriptorium`.
|
|
2. Confirm public package tests do not require real provider credentials.
|
|
3. Confirm `go test ./...` passes.
|
|
4. Confirm the render smoke command passes.
|
|
5. Confirm non-roadmap docs describe only implemented behavior.
|
|
6. Confirm no public result or rendered/prepared output exposes raw API-key values.
|
|
7. Confirm `git diff` does not include unrelated CLI/HTTP behavior changes.
|