Compare commits
2 Commits
v1.2.0
...
a90231ce0c
| Author | SHA1 | Date | |
|---|---|---|---|
| a90231ce0c | |||
| ed879b8bb0 |
@@ -225,7 +225,9 @@ For each `pipeline.scriptorium.artifacts.<name>`:
|
||||
| `output_path` | string | Conditional | required when enabled; also required when referenced by publish/output/input rules |
|
||||
| `timeout` | duration | No | artifact override |
|
||||
| `inputs` | map | No | input key names must be non-empty |
|
||||
| `vars` | map | No | values must be string or bool |
|
||||
| `vars` | map | No | values must be string or bool; `session_id` is reserved and overwritten by Narratio |
|
||||
|
||||
Narratio adds `session_id=narratio-session-<session_id>` to every Scriptorium request for sticky upstream LLM routing. If an artifact config sets `vars.session_id`, Narratio replaces that value before invoking Scriptorium. Use a different variable name if a prompt needs the raw Narratio session ID as content.
|
||||
|
||||
For each artifact input `pipeline.scriptorium.artifacts.<name>.inputs.<input_name>`:
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ Render behavior:
|
||||
|
||||
## Deterministic Behavior
|
||||
- input and var maps are sorted into deterministic `--input` and `--var` CLI args.
|
||||
- stage wiring adds `session_id=narratio-session-<session_id>` to every Scriptorium request for sticky upstream routing, overriding any configured `vars.session_id`.
|
||||
- generated invocation YAML (`scriptorium.generated.v1`) is emitted when requested.
|
||||
- adapter is stateless and does not own artifact-selection policy.
|
||||
|
||||
|
||||
@@ -51,8 +51,3 @@ func Execute(args []string, stdout, stderr io.Writer) int {
|
||||
func printUsage(w io.Writer) {
|
||||
fmt.Fprintf(w, "Usage: narratio <%s>\n", strings.Join(supportedCommands, "|"))
|
||||
}
|
||||
|
||||
func placeholder(out io.Writer, command string) error {
|
||||
_, err := fmt.Fprintf(out, "narratio %s: not yet implemented\n", command)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package artifacts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// S3Store is a placeholder for future remote artifact persistence support.
|
||||
type S3Store struct {
|
||||
Bucket string
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// SessionPathsFor is not implemented for S3-backed storage.
|
||||
func (s *S3Store) SessionPathsFor(_, _ string) SessionPaths {
|
||||
return SessionPaths{}
|
||||
}
|
||||
|
||||
// EnsureLayoutFor returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) EnsureLayoutFor(_, _ string) (SessionPaths, error) {
|
||||
return SessionPaths{}, fmt.Errorf("artifacts s3 ensure layout for campaign/session: not yet implemented")
|
||||
}
|
||||
|
||||
// CopyInputFor returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) CopyInputFor(_, _, _, _ string) (Ref, error) {
|
||||
return Ref{}, fmt.Errorf("artifacts s3 copy input for campaign/session: not yet implemented")
|
||||
}
|
||||
|
||||
// Exists returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) Exists(_ string) (bool, error) {
|
||||
return false, fmt.Errorf("artifacts s3 exists: not yet implemented")
|
||||
}
|
||||
|
||||
// ExistsRef returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) ExistsRef(_ Ref) (bool, error) {
|
||||
return false, fmt.Errorf("artifacts s3 exists ref: not yet implemented")
|
||||
}
|
||||
|
||||
// WriteFileAtomic returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) WriteFileAtomic(_ string, _ []byte, _ os.FileMode) error {
|
||||
return fmt.Errorf("artifacts s3 write file atomic: not yet implemented")
|
||||
}
|
||||
|
||||
// Checksum returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) Checksum(_ string) (string, error) {
|
||||
return "", fmt.Errorf("artifacts s3 checksum: not yet implemented")
|
||||
}
|
||||
|
||||
// AcquireSessionLockFor returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) AcquireSessionLockFor(_, _ string) (*LockHandle, error) {
|
||||
return nil, fmt.Errorf("artifacts s3 acquire lock for campaign/session: not yet implemented")
|
||||
}
|
||||
|
||||
// ReleaseSessionLock returns a not-yet-implemented error in the scaffold.
|
||||
func (s *S3Store) ReleaseSessionLock(_ *LockHandle) error {
|
||||
return fmt.Errorf("artifacts s3 release lock: not yet implemented")
|
||||
}
|
||||
@@ -34,9 +34,11 @@ const (
|
||||
DefaultAuditaTimeout = "3h"
|
||||
DefaultAuditaReport = true
|
||||
|
||||
DefaultScriptoriumBinary = "scriptorium"
|
||||
DefaultScriptoriumTimeout = "10m"
|
||||
DefaultScriptoriumArtifactOutputRoot = "artifacts"
|
||||
DefaultScriptoriumBinary = "scriptorium"
|
||||
DefaultScriptoriumTimeout = "10m"
|
||||
DefaultScriptoriumArtifactOutputRoot = "artifacts"
|
||||
DefaultScriptoriumStickySessionVarName = "session_id"
|
||||
DefaultScriptoriumStickySessionVarPrefix = "narratio-session-"
|
||||
|
||||
DefaultTrimEnabled = true
|
||||
DefaultTrimOutputPath = artifactmodel.TranscriptPathFinalTrimmed
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package contracts
|
||||
|
||||
// ArtifactResult is a placeholder generated artifact contract.
|
||||
type ArtifactResult struct {
|
||||
Schema string `json:"schema"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package contracts
|
||||
|
||||
// SessionManifest is a placeholder durable session-run contract.
|
||||
type SessionManifest struct {
|
||||
Schema string `json:"schema"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package contracts
|
||||
|
||||
// SpeakerTranscript is a placeholder transcription artifact contract.
|
||||
type SpeakerTranscript struct {
|
||||
Schema string `json:"schema"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// CanonicalTranscript is a placeholder merged transcript contract.
|
||||
type CanonicalTranscript struct {
|
||||
Schema string `json:"schema"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// ProcessedTranscript is a placeholder polished transcript contract.
|
||||
type ProcessedTranscript struct {
|
||||
Schema string `json:"schema"`
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
@@ -347,6 +347,7 @@ func executeAnalyzeArtifact(
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("analyze: resolve vars for artifact %q: %w", artifactName, err)
|
||||
}
|
||||
vars = withScriptoriumStickySessionVar(vars, sessionID)
|
||||
|
||||
canonicalOutputPath, err := resolveScriptoriumOutputPath(paths, artifactCfg.OutputPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -54,6 +54,9 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
||||
if req.Timeout != 2*time.Minute {
|
||||
t.Fatalf("timeout = %s, want 2m", req.Timeout)
|
||||
}
|
||||
if req.Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("session_id var = %q, want sticky narratio session id", req.Vars["session_id"])
|
||||
}
|
||||
|
||||
if len(result.Outputs) != 1 || result.Outputs[0].Kind != "session_recap" {
|
||||
t.Fatalf("outputs = %#v, want one session_recap output", result.Outputs)
|
||||
@@ -69,6 +72,34 @@ func TestAnalyzeGeneratesSessionRecapFromTrimmedTranscript(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeStickySessionVarOverridesConfiguredAndPreservesArbitraryVars(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
writeAnalyzeFile(t, filepath.Join(paths.TranscriptsDir, "final.trimmed.json"), `{"segments":[]}`)
|
||||
|
||||
artifact := env.Config.Pipeline.Scriptorium.Artifacts["session_recap"]
|
||||
artifact.Vars = map[string]any{
|
||||
"session_id": "configured-session",
|
||||
"character_name": "Hrank",
|
||||
"character_class": "Fighter",
|
||||
}
|
||||
env.Config.Pipeline.Scriptorium.Artifacts["session_recap"] = artifact
|
||||
|
||||
if _, err := (analyzeStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("Run() error = %v", err)
|
||||
}
|
||||
req := fake.RunRequests[0]
|
||||
if req.Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("session_id var = %q, want sticky value", req.Vars["session_id"])
|
||||
}
|
||||
if req.Vars["character_name"] != "Hrank" {
|
||||
t.Fatalf("character_name var = %q, want Hrank", req.Vars["character_name"])
|
||||
}
|
||||
if req.Vars["character_class"] != "Fighter" {
|
||||
t.Fatalf("character_class var = %q, want Fighter", req.Vars["character_class"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyzeRenderDebugFalseDoesNotCallRenderArtifact(t *testing.T) {
|
||||
env, m, fake := setupAnalyzeEnv(t)
|
||||
paths := sessionPathsForEnv(env, m.SessionID)
|
||||
@@ -142,6 +173,12 @@ func TestAnalyzeRenderOutputPathIsRecorded(t *testing.T) {
|
||||
if len(fake.RenderRequests) != 1 {
|
||||
t.Fatalf("render requests = %d, want 1", len(fake.RenderRequests))
|
||||
}
|
||||
if fake.RenderRequests[0].Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("render session_id var = %q, want sticky narratio session id", fake.RenderRequests[0].Vars["session_id"])
|
||||
}
|
||||
if fake.RunRequests[0].Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("run session_id var = %q, want sticky narratio session id", fake.RunRequests[0].Vars["session_id"])
|
||||
}
|
||||
if result.Metadata["render_output_path"] != filepath.Join(paths.ArtifactsDir, "session_recap.render.json") {
|
||||
t.Fatalf("render_output_path = %#v, want session_recap.render.json path", result.Metadata["render_output_path"])
|
||||
}
|
||||
|
||||
21
internal/stage/scriptorium_vars.go
Normal file
21
internal/stage/scriptorium_vars.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package stage
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
)
|
||||
|
||||
func withScriptoriumStickySessionVar(vars map[string]string, sessionID string) map[string]string {
|
||||
out := make(map[string]string, len(vars)+1)
|
||||
for k, v := range vars {
|
||||
out[k] = v
|
||||
}
|
||||
if trimmedSessionID := strings.TrimSpace(sessionID); trimmedSessionID != "" {
|
||||
out[config.DefaultScriptoriumStickySessionVarName] = config.DefaultScriptoriumStickySessionVarPrefix + trimmedSessionID
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func (trimStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*Stag
|
||||
inputPaths := map[string]string{
|
||||
boundsCfg.TranscriptInputName: normalizedPath,
|
||||
}
|
||||
vars := map[string]string{}
|
||||
vars := withScriptoriumStickySessionVar(nil, sessionID)
|
||||
|
||||
metadata["bounds_prompt_id"] = boundsCfg.PromptID
|
||||
metadata["bounds_profile_id"] = boundsCfg.ProfileID
|
||||
|
||||
@@ -34,6 +34,9 @@ func TestTrimStageConsumesNormalizedAndProducesTrimmedTranscript(t *testing.T) {
|
||||
if len(scr.RunRequests) != 1 {
|
||||
t.Fatalf("scriptorium run requests = %d, want 1", len(scr.RunRequests))
|
||||
}
|
||||
if scr.RunRequests[0].Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("session_id var = %q, want sticky narratio session id", scr.RunRequests[0].Vars["session_id"])
|
||||
}
|
||||
if len(ser.TrimRequests) != 1 {
|
||||
t.Fatalf("seriatim trim requests = %d, want 1", len(ser.TrimRequests))
|
||||
}
|
||||
@@ -129,6 +132,12 @@ func TestTrimStageRenderDebugDiagnosticsAreNotStageOutputs(t *testing.T) {
|
||||
if len(scr.RenderRequests) != 1 {
|
||||
t.Fatalf("scriptorium render requests = %d, want 1", len(scr.RenderRequests))
|
||||
}
|
||||
if scr.RenderRequests[0].Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("render session_id var = %q, want sticky narratio session id", scr.RenderRequests[0].Vars["session_id"])
|
||||
}
|
||||
if scr.RunRequests[0].Vars["session_id"] != "narratio-session-2026-05-03" {
|
||||
t.Fatalf("run session_id var = %q, want sticky narratio session id", scr.RunRequests[0].Vars["session_id"])
|
||||
}
|
||||
for _, out := range result.Outputs {
|
||||
if out.Kind == "session_bounds_render" {
|
||||
t.Fatalf("render diagnostics should not be stage outputs: %#v", result.Outputs)
|
||||
|
||||
Reference in New Issue
Block a user