Implement support for passing a session_id variable to scriptorium to support sticky routing

This commit is contained in:
2026-07-02 21:04:37 -05:00
parent ed879b8bb0
commit a90231ce0c
8 changed files with 78 additions and 5 deletions

View File

@@ -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>`:

View File

@@ -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.

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"])
}

View 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
}

View File

@@ -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

View File

@@ -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)