Implement support for passing a session_id variable to scriptorium to support sticky routing
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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