475 lines
21 KiB
Markdown
475 lines
21 KiB
Markdown
# Scriptorium Cutover Implementation Plan
|
|
|
|
This plan implements the target state in
|
|
[scriptorium.md](scriptorium.md): a hard cutover from Notarius' local
|
|
OpenAI-compatible adapter, local LLM profile schema, and local system/user
|
|
prompt renderer to Scriptorium-backed prompt execution.
|
|
|
|
The target implementer is an LLM coding agent. Complete each stage in order.
|
|
Do not preserve backward compatibility for removed local LLM-profile or
|
|
prompt-rendering behavior unless a stage explicitly says to keep a temporary
|
|
test seam.
|
|
|
|
## Global Constraints
|
|
|
|
- Keep Scriptorium types out of chunk, extract, and normalize module contracts.
|
|
Module-facing requests should use Notarius-owned contract types.
|
|
- Keep provider plumbing behind `internal/framework/llm` and CLI construction.
|
|
- Keep module-owned prompt intent, prompt IDs, response schemas, validators, and
|
|
source-reference validation with the relevant modules.
|
|
- Keep raw input material, raw references, raw prompts, raw schemas, and API
|
|
keys out of manifests and default diagnostics.
|
|
- Treat the original source input and large references as prompt input
|
|
materials. They may be passed to Scriptorium as inline/file inputs, but should
|
|
not be copied into durable provenance.
|
|
- Use Scriptorium profile configuration directly. Do not keep Notarius'
|
|
`llm_profiles` schema as a second profile system.
|
|
- Use Scriptorium structured-output execution and validation behavior for
|
|
production calls. Notarius should still unmarshal successful structured JSON
|
|
into module-owned response structs and run module-owned deterministic
|
|
validation afterward.
|
|
|
|
## Stage 1: Dependency And API Grounding
|
|
|
|
Goal: add the Scriptorium dependency and establish the exact public API surface
|
|
used by the rest of the implementation.
|
|
|
|
Work:
|
|
|
|
- Add `gitea.maximumdirect.net/eric/scriptorium` to `go.mod` with `go get`.
|
|
- Inspect the installed package with `go doc` or source reads before coding the
|
|
adapter. Confirm names and fields for:
|
|
- `NewEngine`;
|
|
- `Config`;
|
|
- `WithPromptFS`, `WithPromptFile`, or equivalent prompt source options;
|
|
- `WithProfileFile`, `WithProfileFS`, profile directory config, and built-in
|
|
profile behavior;
|
|
- `WithSchemaFS` or equivalent schema source options;
|
|
- `RunRequest`, including prompt ID, prompt version, profile ID, inputs,
|
|
variables, metadata, direct API key, and validation override fields;
|
|
- `RunResult`, including raw output, output artifact, validation status,
|
|
prompt/profile/model metadata, usage, and timings;
|
|
- public sentinel errors.
|
|
- Add a short internal implementation note as a code comment only where needed;
|
|
do not add user-facing docs in this stage except if a package-level test
|
|
helper needs explanation.
|
|
|
|
Acceptance checks:
|
|
|
|
- `go test ./internal/framework/llm`
|
|
- `go test ./internal/cli`
|
|
- `go test ./...`
|
|
|
|
## Stage 2: Prompt Input Materials And Session IDs
|
|
|
|
Goal: make raw source input and references available to module prompt execution
|
|
without storing large content in manifests or default diagnostics.
|
|
|
|
Work:
|
|
|
|
- Add Notarius-owned contract types under `internal/framework/contracts`:
|
|
- `LLMInputMaterial` with at least `Name`, `MediaType`, `Content`, `Digest`,
|
|
`OriginURI`, and `SizeBytes`;
|
|
- a helper-friendly collection type, such as `LLMInputSet`, if useful.
|
|
- Add prompt-execution fields to `StructuredCompletionRequest`:
|
|
- `PromptID`;
|
|
- `PromptVersion`;
|
|
- `ProfileID` or reuse the request `LLMProfile` value when called from stage
|
|
requests;
|
|
- `SessionID`;
|
|
- `Inputs map[string]LLMInputMaterial`;
|
|
- `Vars map[string]any`.
|
|
- Keep or remove the old `Messages`, `Model`, `ResponseSchemaName`, and
|
|
`ResponseSchema` fields according to what makes the cutover cleanest. The
|
|
final production path must not require modules to pass rendered messages or
|
|
raw schema JSON directly to the provider adapter.
|
|
- Add `SourceInput contracts.LLMInputMaterial` and `SessionID string` to
|
|
`ChunkRequest`, `ExtractionRequest`, and `NormalizeRequest`.
|
|
- Add `SessionID string` to `pipeline.RunInput`.
|
|
- In `pipeline.Run`, build a source input material from `RunInput.RawInput` and
|
|
`RunInput.Path`:
|
|
- preserve the bytes exactly;
|
|
- infer media type from the input path extension, using `application/json`
|
|
for `.json` and a deterministic fallback for unknown extensions;
|
|
- compute a SHA-256 digest;
|
|
- use a file URI or path-derived origin URI;
|
|
- do not put the bytes in manifest metadata.
|
|
- Pass the same source input material and session ID to chunk, extract, and
|
|
normalize requests.
|
|
- Reuse existing materialized `ReferenceSet` content for reference prompt
|
|
inputs. Do not introduce a second file-reading path for references.
|
|
- Add CLI support for `--session-id <id>` on `notarius run`.
|
|
- Empty means use a deterministic default derived from the parsed source
|
|
document ID.
|
|
- The deterministic default must be stable across runs over the same parsed
|
|
source document.
|
|
- The explicit value should be trimmed and rejected if empty after trimming.
|
|
- Because the default depends on the parsed source document, compute and attach
|
|
the final session ID inside the runner after input parsing, or return enough
|
|
information from parsing for the CLI to resolve it before LLM calls.
|
|
- Record the non-secret session ID in run metadata or manifest metadata, but
|
|
never record raw prompt content.
|
|
|
|
Acceptance tests:
|
|
|
|
- Contract tests proving `LLMInputMaterial.Content` is defensively copied where
|
|
relevant and omitted from JSON.
|
|
- Pipeline tests proving chunk, extract, and normalize requests receive the same
|
|
source input bytes and session ID.
|
|
- CLI tests for `--session-id`, including explicit value, missing value,
|
|
trimming, and default behavior.
|
|
- Redaction/diagnostics tests proving raw source bytes and reference content do
|
|
not appear in manifests or default diagnostics.
|
|
|
|
Focused checks:
|
|
|
|
- `go test ./internal/framework/contracts`
|
|
- `go test ./internal/framework/pipeline`
|
|
- `go test ./internal/cli`
|
|
|
|
## Stage 3: Configuration Hard Cutover To Scriptorium Profiles
|
|
|
|
Goal: replace Notarius' local LLM profile schema with Scriptorium profile
|
|
selection and profile sources.
|
|
|
|
Work:
|
|
|
|
- Bump the Notarius file config version because this is an incompatible config
|
|
schema change.
|
|
- Remove the top-level `llm_profiles` file-config schema and the local
|
|
`config.LLMProfile` model.
|
|
- Add a top-level Scriptorium config block. Use this shape unless Scriptorium's
|
|
installed API requires a small naming adjustment:
|
|
|
|
```yaml
|
|
scriptorium:
|
|
profile_dir: ./profiles
|
|
profile_file: ./profiles.yml
|
|
```
|
|
|
|
- Treat `profile_dir` and `profile_file` as mutually exclusive in Notarius
|
|
config validation. Built-in Scriptorium profiles remain available when neither
|
|
is set.
|
|
- Keep existing module binding field name `llm_profile`; it now names a
|
|
Scriptorium profile ID.
|
|
- Stop forcing empty module `llm_profile` bindings to
|
|
`pipeline.DefaultLLMProfile`. Empty means "use the Scriptorium prompt's
|
|
`default_profile`."
|
|
- Keep `--llm-profile` as a run-level operational override. It should set the
|
|
same explicit Scriptorium profile ID for every LLM-eligible selected target.
|
|
- Remove `OpenAICompatibleClientConfig` construction from `internal/core/config`.
|
|
- Remove local OpenAI-compatible provider validation from config validation.
|
|
- Add config validation for:
|
|
- config version;
|
|
- mutually exclusive `scriptorium.profile_dir` and
|
|
`scriptorium.profile_file`;
|
|
- non-empty profile source paths when fields are present;
|
|
- non-empty explicit `llm_profile` strings after trimming.
|
|
- Add CLI/config validation that uses Scriptorium profile loading to reject
|
|
unknown explicit profile IDs when possible. Prompt-default profile failures
|
|
may surface during Scriptorium prepare/run if Scriptorium owns that lookup.
|
|
- Remove or replace environment override behavior tied to
|
|
`NOTARIUS_LLM_DEFAULT_*`. Secrets should come from Scriptorium profile
|
|
`api_key_env` values or direct request-scoped Scriptorium behavior.
|
|
- Update redacted effective config behavior for the new Scriptorium config
|
|
shape.
|
|
|
|
Acceptance tests:
|
|
|
|
- Config parsing accepts `scriptorium.profile_dir`.
|
|
- Config parsing accepts `scriptorium.profile_file`.
|
|
- Config validation rejects both fields set at once.
|
|
- Config validation rejects stale `llm_profiles`.
|
|
- Existing pipeline bindings with explicit `llm_profile` resolve to trimmed
|
|
Scriptorium profile IDs.
|
|
- Empty `llm_profile` remains empty through resolution unless `--llm-profile`
|
|
is supplied.
|
|
- CLI config validation fails cleanly for an unknown explicit profile ID.
|
|
- Redacted config diagnostics do not contain raw API keys.
|
|
|
|
Focused checks:
|
|
|
|
- `go test ./internal/core/config`
|
|
- `go test ./internal/cli`
|
|
|
|
## Stage 4: Scriptorium Prompt And Schema Assets
|
|
|
|
Goal: move production LLM prompts and schemas to Scriptorium-compatible assets
|
|
while preserving module ownership.
|
|
|
|
Work:
|
|
|
|
- Create a prompt/schema asset registration mechanism that does not put
|
|
D&D-specific prompt content in framework packages.
|
|
- Recommended shape: add a small prompt-asset registry in
|
|
`internal/framework/llm` or a sibling framework package that can collect
|
|
`fs.FS` roots for Scriptorium prompt and schema sources.
|
|
- Production module packages should register their own Scriptorium prompt and
|
|
schema assets through production catalog/registry wiring in `internal/cli`.
|
|
- Shared D&D prompt assets may live in a D&D-specific module package such as
|
|
`internal/modules/dnd/promptassets`; they must not live in `internal/core`
|
|
or source-agnostic framework packages.
|
|
- Add shared D&D prompt assets:
|
|
- stable shared system message, if needed;
|
|
- stable cacheable transcript user message:
|
|
|
|
```text
|
|
A transcript of a Dungeons & Dragons gameplay session is provided below.
|
|
|
|
{{ input "transcript" }}
|
|
```
|
|
|
|
- stable cacheable reference/context message templates for roster, glossary,
|
|
previous recap, or other large references used by current D&D modules.
|
|
- Convert `dnd/scenes` to a Scriptorium prompt definition:
|
|
- prompt ID: `dnd.scenes`;
|
|
- prompt version: current module prompt version;
|
|
- input `transcript`, required, `application/json`;
|
|
- messages ordered for cache reuse: shared system, shared transcript user
|
|
message with cache control, scene task, scene instructions;
|
|
- output JSON schema path pointing at the existing scene schema asset;
|
|
- schema IDs/names/versions remain module-owned and manifest-safe.
|
|
- Convert `dnd/spells` to a Scriptorium prompt definition:
|
|
- prompt ID: `dnd.spells`;
|
|
- prompt version: current module prompt version;
|
|
- input `transcript`, required, `application/json`;
|
|
- optional inputs for `roster` and `glossary`;
|
|
- messages ordered for cache reuse: shared system, shared transcript user
|
|
message with cache control, optional/reference context message with cache
|
|
control, spell task, spell instructions;
|
|
- output JSON schema path pointing at the existing spell schema asset.
|
|
- For optional references, pass empty inline input material when the slot is
|
|
unbound unless Scriptorium's template/input semantics support missing
|
|
optional inputs cleanly. Do not let optional missing references make prompt
|
|
rendering fail.
|
|
- For multiple reference items in one slot, concatenate deterministically with
|
|
stable headings that include only non-secret provenance, then pass the result
|
|
as that prompt input. Existing single-item slots should keep their current
|
|
behavior.
|
|
- Replace current prompt hash metadata with hashes derived from the
|
|
Scriptorium prompt definition plus message assets, or with Scriptorium
|
|
prepared-run prompt metadata if it is available without raw prompt content.
|
|
- Preserve existing manifest metadata keys where practical:
|
|
- `prompt_id`;
|
|
- `prompt_version`;
|
|
- `prompt_sha256`;
|
|
- `response_schema_key`;
|
|
- `response_schema_id`;
|
|
- `response_schema_name`;
|
|
- `response_schema_version`;
|
|
- `response_schema_sha256`.
|
|
|
|
Acceptance tests:
|
|
|
|
- Prompt asset loading fails fast for missing prompt files or schemas.
|
|
- `dnd/scenes` prepared prompt contains separate transcript and task messages.
|
|
- `dnd/spells` prepared prompt contains separate transcript, reference, and
|
|
task messages.
|
|
- The transcript message body is byte-identical to the expected shared template
|
|
plus original Seriatim JSON bytes.
|
|
- Reference prompt input rendering is deterministic.
|
|
- Prompt/schema diagnostics omit raw prompt text, raw transcript bytes, raw
|
|
reference content, and raw schema JSON.
|
|
|
|
Focused checks:
|
|
|
|
- `go test ./internal/framework/llm`
|
|
- `go test ./internal/modules/chunk/dnd/scenes`
|
|
- `go test ./internal/modules/extract/dnd/spells`
|
|
|
|
## Stage 5: Scriptorium-Backed LLM Runtime
|
|
|
|
Goal: implement the production `StructuredLLMClient` using Scriptorium.
|
|
|
|
Work:
|
|
|
|
- Add a Scriptorium-backed client under `internal/framework/llm`.
|
|
- Its constructor should accept:
|
|
- Scriptorium profile source settings from effective Notarius config;
|
|
- registered prompt/schema assets;
|
|
- request timeout or HTTP client settings only if still owned by Notarius
|
|
after the profile cutover;
|
|
- optional Scriptorium engine options for tests.
|
|
- Implement `CompleteStructured(ctx, req, out)` by:
|
|
- validating `out` is a non-nil pointer;
|
|
- validating `req.PromptID` is non-empty;
|
|
- converting Notarius `LLMInputMaterial` values to Scriptorium artifact refs;
|
|
- adding `session_id` to Scriptorium request vars when non-empty;
|
|
- passing explicit profile ID only when the module binding or CLI override
|
|
supplied one;
|
|
- passing no raw API key unless a deliberate request-scoped secret path is
|
|
implemented;
|
|
- calling Scriptorium `Run`;
|
|
- converting final validation failure into a Notarius error;
|
|
- unmarshaling successful structured JSON into `out`;
|
|
- returning `StructuredCompletionResponse` with raw JSON content, provider,
|
|
model, profile ID when available, token usage, and non-secret metadata.
|
|
- Add a profile recorder or response accumulator so `RunManifest.LLMProfiles`
|
|
records the actual Scriptorium profile/provider/model values used during the
|
|
run.
|
|
- Do not rely on one precomputed profile ID before pipeline execution.
|
|
- Deduplicate profile manifest entries deterministically.
|
|
- Keep the existing `Scheduler` and scheduled client wrapper unless Scriptorium
|
|
provides an equivalent Notarius-approved concurrency mechanism.
|
|
- Ensure all Scriptorium errors are wrapped with context and converted to
|
|
concise CLI-facing errors. Preserve `errors.Is` checks internally when
|
|
practical.
|
|
- Apply Notarius secret redaction to errors before writing diagnostics.
|
|
- Remove production construction of `OpenAICompatibleClient`.
|
|
|
|
Acceptance tests:
|
|
|
|
- Scriptorium adapter maps Notarius prompt request fields into the expected
|
|
Scriptorium run request using an injected fake Scriptorium LLM client.
|
|
- Successful structured output unmarshals into the caller target.
|
|
- Scriptorium validation failure returns an error.
|
|
- Provider/runtime failure returns an error with operation context.
|
|
- Context cancellation is respected.
|
|
- Token usage maps into `StructuredCompletionResponse`.
|
|
- Used Scriptorium profile/provider/model metadata appears in the run manifest
|
|
without secrets.
|
|
- API keys or bearer tokens in synthetic errors are redacted.
|
|
- Scheduler still bounds concurrent Scriptorium-backed calls.
|
|
|
|
Focused checks:
|
|
|
|
- `go test ./internal/framework/llm`
|
|
- `go test ./internal/framework/pipeline`
|
|
- `go test ./internal/cli`
|
|
|
|
## Stage 6: Module Cutover And Legacy Runtime Removal
|
|
|
|
Goal: update production modules to call the prompt-based contract and remove
|
|
obsolete local prompt/runtime code.
|
|
|
|
Work:
|
|
|
|
- Update `dnd/scenes`:
|
|
- stop rendering local system/user prompt strings;
|
|
- call `CompleteStructured` with `PromptID`, `PromptVersion`, `SessionID`,
|
|
`ProfileID` or request LLM profile, `transcript` input material, and any
|
|
required vars;
|
|
- keep existing response validation, chunk canonicalization, caveat handling,
|
|
and manifest metadata policy.
|
|
- Update `dnd/spells`:
|
|
- stop rendering local system/user prompt strings;
|
|
- call `CompleteStructured` with `transcript`, optional `roster`, optional
|
|
`glossary`, session ID, profile ID, and vars;
|
|
- keep existing spell response validation, source-reference validation, and
|
|
manifest metadata policy.
|
|
- Update any LLM-backed normalize modules if present. If only noop normalize is
|
|
present, ensure the contract and tests prove normalizers can receive the same
|
|
Scriptorium-capable client and prompt inputs.
|
|
- Remove the old `internal/framework/prompt` renderer if no remaining code uses
|
|
it. If generic tests still need prompt rendering, replace them with
|
|
Scriptorium prompt asset tests or delete obsolete tests.
|
|
- Remove `internal/framework/llm/openai_compatible_client.go` and its tests
|
|
after the Scriptorium adapter tests cover replacement behavior.
|
|
- Remove local OpenAI-compatible integration docs after current-behavior docs
|
|
are updated in Stage 7.
|
|
- Remove stale schema registry helpers only if they are no longer needed for
|
|
module-owned schema metadata. Keep lightweight schema hashing/loading helpers
|
|
if modules still use them for manifest metadata.
|
|
|
|
Acceptance tests:
|
|
|
|
- `dnd/scenes` fake-client tests assert the module sends prompt ID,
|
|
transcript input, session ID, and schema/prompt metadata rather than rendered
|
|
message text.
|
|
- `dnd/spells` fake-client tests assert roster/glossary inputs are passed as
|
|
inputs and not interpolated locally.
|
|
- Existing malformed LLM response tests still fail as malformed structured
|
|
output.
|
|
- Existing source-reference validation tests still pass.
|
|
- No production code imports the old prompt renderer or local
|
|
OpenAI-compatible client.
|
|
|
|
Focused checks:
|
|
|
|
- `go test ./internal/modules/chunk/dnd/scenes`
|
|
- `go test ./internal/modules/extract/dnd/spells`
|
|
- If `internal/framework/prompt` is deleted, do not run a package-specific test
|
|
for it; instead verify with `rg -n "internal/framework/prompt|RenderUserSystem" internal`
|
|
that no production code still depends on it.
|
|
|
|
## Stage 7: CLI, Examples, Docs, And Full Validation
|
|
|
|
Goal: finish user-facing behavior, examples, and canonical docs for the new
|
|
runtime.
|
|
|
|
Work:
|
|
|
|
- Update `examples/dnd-spells.config.yml` and any maintained test configs to
|
|
the new config version and Scriptorium profile source behavior.
|
|
- Update `docs/config.md`:
|
|
- new config version;
|
|
- `scriptorium.profile_dir` and `scriptorium.profile_file`;
|
|
- `llm_profile` now means Scriptorium profile ID;
|
|
- empty `llm_profile` behavior;
|
|
- removed `llm_profiles`;
|
|
- removed `NOTARIUS_LLM_DEFAULT_*` env behavior if removed;
|
|
- secret-handling policy through Scriptorium profile `api_key_env`.
|
|
- Update `docs/cli.md`:
|
|
- add `--session-id`;
|
|
- update `--llm-profile` wording to Scriptorium profile override;
|
|
- update run examples if needed.
|
|
- Update `docs/internal/llm.md`:
|
|
- Scriptorium-backed runtime;
|
|
- prompt asset loading;
|
|
- input material handling;
|
|
- scheduling;
|
|
- structured-output validation;
|
|
- profile manifest recording;
|
|
- secret redaction.
|
|
- Update `docs/internal/modules.md`:
|
|
- module prompt ownership through Scriptorium prompt definitions;
|
|
- chunk/extract/normalize request input materials and session ID;
|
|
- D&D scenes/spells prompt metadata.
|
|
- Update `docs/internal/pipeline.md`:
|
|
- raw input material lifecycle;
|
|
- reference material lifecycle into prompt inputs;
|
|
- session ID lifecycle;
|
|
- actual LLM profile provenance.
|
|
- Update `docs/integrations/json-output.md` for any manifest changes.
|
|
- Remove or rewrite `docs/integrations/openai-compatible.md`. If no local
|
|
OpenAI-compatible adapter remains, do not document it as current Notarius
|
|
behavior.
|
|
- Update `docs/troubleshooting.md` for:
|
|
- Scriptorium profile-not-found;
|
|
- prompt-not-found;
|
|
- schema/validation failures;
|
|
- missing API key env values;
|
|
- session ID usage if relevant.
|
|
- Update `docs/operations.md` if diagnostics or retention behavior changes.
|
|
- Remove the Scriptorium migration item from `docs/roadmap/future.md` once the
|
|
feature is implemented.
|
|
- Replace `docs/roadmap/implementation.md` with a completed note or remove it
|
|
after implementation is complete, according to the repository's current
|
|
roadmap cleanup pattern.
|
|
|
|
Acceptance tests and inspections:
|
|
|
|
- `rg -n "llm_profiles|OpenAICompatibleClient|openai-compatible|RenderUserSystem|NOTARIUS_LLM_DEFAULT" internal docs examples`
|
|
should return only intentional historical/deferred references, if any.
|
|
- `rg -n "scriptorium|session-id|profile_dir|profile_file" docs examples`
|
|
should show current-behavior docs and examples are updated.
|
|
- `go test ./...`
|
|
- `go vet ./...`
|
|
- `go build ./cmd/notarius`
|
|
|
|
## Cross-Stage Review Checklist
|
|
|
|
Before considering the implementation complete, verify:
|
|
|
|
- No Scriptorium public types appear in chunk, extract, or normalize contracts.
|
|
- No raw source input, reference content, prompt text, schema JSON, API key, or
|
|
bearer token appears in manifests or default diagnostics.
|
|
- The shared transcript message uses original Seriatim JSON bytes exactly.
|
|
- Large reference content can be supplied as Scriptorium prompt inputs without
|
|
changing module-facing reference slot contracts.
|
|
- `--session-id` is easy for an external orchestrator to pass.
|
|
- Empty `llm_profile` lets prompt defaults work; explicit `llm_profile` and
|
|
`--llm-profile` select Scriptorium profile IDs.
|
|
- Production Notarius LLM execution goes through Scriptorium.
|
|
- Current-behavior documentation describes only implemented behavior.
|