Derive stable prompt sessions for CLI runs
This commit is contained in:
@@ -153,10 +153,10 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
resume := fs.Bool("resume", false, "reuse compatible recorded checkpoints")
|
resume := fs.Bool("resume", false, "reuse compatible recorded checkpoints")
|
||||||
recomputeStep := singleValueFlag{name: "--recompute-step"}
|
recomputeStep := singleValueFlag{name: "--recompute-step"}
|
||||||
chunkCache := chunkCacheFlag{}
|
chunkCache := chunkCacheFlag{}
|
||||||
sessionID := sessionIDFlag{}
|
requestedSessionID := sessionIDFlag{}
|
||||||
referenceFlags := stringListFlag{}
|
referenceFlags := stringListFlag{}
|
||||||
withoutReferenceFlags := stringListFlag{}
|
withoutReferenceFlags := stringListFlag{}
|
||||||
fs.Var(&sessionID, "session-id", "prompt session identifier")
|
fs.Var(&requestedSessionID, "session-id", "prompt session identifier")
|
||||||
fs.Var(&reasoningEffort, "reasoning-effort", "reasoning effort override")
|
fs.Var(&reasoningEffort, "reasoning-effort", "reasoning effort override")
|
||||||
fs.Var(&chunkCache, "chunk_cache", "chunk plan cache mode: auto, bypass, or refresh")
|
fs.Var(&chunkCache, "chunk_cache", "chunk plan cache mode: auto, bypass, or refresh")
|
||||||
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, merge.slot=path, lane.slot=path, lane.extract.slot=path, lane.merge.slot=path, or lane.normalize.slot=path")
|
fs.Var(&referenceFlags, "reference", "reference binding, as slot=path, chunk.slot=path, merge.slot=path, lane.slot=path, lane.extract.slot=path, lane.merge.slot=path, or lane.normalize.slot=path")
|
||||||
@@ -199,7 +199,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
fmt.Fprintln(stderr, "notarius: --debug-dir must not be empty")
|
fmt.Fprintln(stderr, "notarius: --debug-dir must not be empty")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if sessionID.set && strings.TrimSpace(sessionID.value) == "" {
|
if requestedSessionID.set && strings.TrimSpace(requestedSessionID.value) == "" {
|
||||||
fmt.Fprintln(stderr, "notarius: --session-id must not be empty")
|
fmt.Fprintln(stderr, "notarius: --session-id must not be empty")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
@@ -414,11 +414,15 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, fmt.Errorf("read input %q: %w", strings.TrimSpace(*inputPath), err))
|
return failPipelineCommand(stderr, commandState, terminalWriter, fmt.Errorf("read input %q: %w", strings.TrimSpace(*inputPath), err))
|
||||||
}
|
}
|
||||||
|
effectiveSessionID, err := resolvePromptSessionID(requestedSessionID.value, effective.ResolvedPipeline.Input.Module, rawInput)
|
||||||
|
if err != nil {
|
||||||
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
|
}
|
||||||
chunkPlans, err := chunkPlanStoreForRun(effective.Config.Cache.ChunkPlans, opts)
|
chunkPlans, err := chunkPlanStoreForRun(effective.Config.Cache.ChunkPlans, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(*llmProfile), strings.TrimSpace(sessionID.value), runtimeOverrides, *resume)
|
checkpointRecorder, checkpointLoader, err := checkpointHandlersForRun(effective.Config.Cache.Checkpoints, opts, effective.ResolvedPipeline, prepared.CheckpointFingerprints(), llmFingerprints, rawInput, only, llmProfiles, strings.TrimSpace(*llmProfile), effectiveSessionID, runtimeOverrides, *resume)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
@@ -427,7 +431,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
Prepared: prepared,
|
Prepared: prepared,
|
||||||
Path: strings.TrimSpace(*inputPath),
|
Path: strings.TrimSpace(*inputPath),
|
||||||
RawInput: rawInput,
|
RawInput: rawInput,
|
||||||
SessionID: strings.TrimSpace(sessionID.value),
|
SessionID: effectiveSessionID,
|
||||||
RunID: runID,
|
RunID: runID,
|
||||||
StartedAt: startedAt,
|
StartedAt: startedAt,
|
||||||
LLMProfiles: llmProfiles,
|
LLMProfiles: llmProfiles,
|
||||||
|
|||||||
@@ -492,17 +492,28 @@ func TestEffectiveLLMProfileIDsAreSortedDeduplicatedAndLLMOnly(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunSessionIDUsesExplicitValueOrSourceDocumentID(t *testing.T) {
|
func TestRunSessionIDUsesEffectiveValueForPromptRequests(t *testing.T) {
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
name string
|
name string
|
||||||
args []string
|
args []string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{name: "source document", want: "source"},
|
{name: "derived default"},
|
||||||
{name: "explicit trimmed value", args: []string{"--session-id", " explicit-session "}, want: "explicit-session"},
|
{name: "explicit trimmed value", args: []string{"--session-id", " explicit-session "}, want: "explicit-session"},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
roots := newStateTestRoots(t)
|
roots := newStateTestRoots(t)
|
||||||
|
want := tt.want
|
||||||
|
if want == "" {
|
||||||
|
rawInput, err := os.ReadFile(roots.input)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want, err = resolvePromptSessionID("", "test/input", rawInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
harness := newStateTestHarness()
|
harness := newStateTestHarness()
|
||||||
args := append([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass"}, tt.args...)
|
args := append([]string{"run", "sample", "--config", roots.config, "--input", roots.input, "--chunk_cache", "bypass"}, tt.args...)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
@@ -517,8 +528,8 @@ func TestRunSessionIDUsesExplicitValueOrSourceDocumentID(t *testing.T) {
|
|||||||
t.Fatalf("session IDs = %#v, want all prompt-facing module requests", sessions)
|
t.Fatalf("session IDs = %#v, want all prompt-facing module requests", sessions)
|
||||||
}
|
}
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
if session != tt.want {
|
if session != want {
|
||||||
t.Fatalf("session IDs = %#v, want %q", sessions, tt.want)
|
t.Fatalf("session IDs = %#v, want %q", sessions, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
26
internal/cli/session.go
Normal file
26
internal/cli/session.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const generatedSessionIDPrefix = "notarius:v1:"
|
||||||
|
|
||||||
|
func resolvePromptSessionID(explicitSessionID, inputModule string, rawInput []byte) (string, error) {
|
||||||
|
inputModule = strings.TrimSpace(inputModule)
|
||||||
|
if inputModule == "" {
|
||||||
|
return "", fmt.Errorf("resolve prompt session: input module key must not be empty")
|
||||||
|
}
|
||||||
|
if sessionID := strings.TrimSpace(explicitSessionID); sessionID != "" {
|
||||||
|
return sessionID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
_, _ = hasher.Write([]byte(inputModule))
|
||||||
|
_, _ = hasher.Write([]byte{0})
|
||||||
|
_, _ = hasher.Write(rawInput)
|
||||||
|
return generatedSessionIDPrefix + hex.EncodeToString(hasher.Sum(nil)), nil
|
||||||
|
}
|
||||||
61
internal/cli/session_test.go
Normal file
61
internal/cli/session_test.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestResolvePromptSessionIDUsesVersionedInputIdentity(t *testing.T) {
|
||||||
|
got, err := resolvePromptSessionID("", " seriatim/input/transcript ", []byte("{\"entries\":[\"one\"]}\n"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const want = "notarius:v1:e15fefdca48653e73248b4157900547be1fd34250c0138f8d3d1f2e89b43bb25"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("resolved session = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePromptSessionIDStabilityAndOverride(t *testing.T) {
|
||||||
|
rawInput := []byte("same input")
|
||||||
|
baseline, err := resolvePromptSessionID("", "input/transcript", rawInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
repeated, err := resolvePromptSessionID("", "input/transcript", rawInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if baseline != repeated {
|
||||||
|
t.Fatalf("resolved sessions = %q and %q, want stable value", baseline, repeated)
|
||||||
|
}
|
||||||
|
|
||||||
|
differentModule, err := resolvePromptSessionID("", "input/other", rawInput)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if baseline == differentModule {
|
||||||
|
t.Fatalf("resolved sessions = %q for distinct input modules", baseline)
|
||||||
|
}
|
||||||
|
|
||||||
|
differentInput, err := resolvePromptSessionID("", "input/transcript", []byte("same inpuu"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if baseline == differentInput {
|
||||||
|
t.Fatalf("resolved sessions = %q for distinct input bytes", baseline)
|
||||||
|
}
|
||||||
|
|
||||||
|
override, err := resolvePromptSessionID(" explicit-session ", "input/other", []byte("different input"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if override != "explicit-session" {
|
||||||
|
t.Fatalf("resolved override = %q, want %q", override, "explicit-session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolvePromptSessionIDRejectsEmptyInputModule(t *testing.T) {
|
||||||
|
for _, explicitSessionID := range []string{"", "explicit-session"} {
|
||||||
|
if _, err := resolvePromptSessionID(explicitSessionID, " \t", []byte("input")); err == nil {
|
||||||
|
t.Fatalf("resolvePromptSessionID(%q) error = nil, want empty module failure", explicitSessionID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -191,7 +191,7 @@ func (r *Runner) Run(ctx context.Context, input RunInput) (output RunOutput, err
|
|||||||
return failOutput(output), fmt.Errorf("write source debug artifact: %w", err)
|
return failOutput(output), fmt.Errorf("write source debug artifact: %w", err)
|
||||||
}
|
}
|
||||||
sourceInput := sourceInputMaterial(input.Path, input.RawInput)
|
sourceInput := sourceInputMaterial(input.Path, input.RawInput)
|
||||||
sessionID := resolvedSessionID(input.SessionID, doc.ID)
|
sessionID := strings.TrimSpace(input.SessionID)
|
||||||
output.Manifest.Metadata, err = manifestMetadataWithSessionID(output.Manifest.Metadata, sessionID)
|
output.Manifest.Metadata, err = manifestMetadataWithSessionID(output.Manifest.Metadata, sessionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return failOutput(output), err
|
return failOutput(output), err
|
||||||
@@ -896,13 +896,6 @@ func sourceInputOriginURI(inputPath string) string {
|
|||||||
return fileURI(inputPath)
|
return fileURI(inputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvedSessionID(explicit string, sourceDocumentID string) string {
|
|
||||||
if trimmed := strings.TrimSpace(explicit); trimmed != "" {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(sourceDocumentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func manifestMetadataWithSessionID(metadata map[string]any, sessionID string) (map[string]any, error) {
|
func manifestMetadataWithSessionID(metadata map[string]any, sessionID string) (map[string]any, error) {
|
||||||
out, err := cloneMetadata(metadata)
|
out, err := cloneMetadata(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
37
internal/framework/pipeline/runner_session_test.go
Normal file
37
internal/framework/pipeline/runner_session_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package pipeline
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunnerUsesProvidedSessionWithoutSourceFallback(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
session string
|
||||||
|
wantSet bool
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "provided", session: " supplied-session ", wantSet: true, want: "supplied-session"},
|
||||||
|
{name: "empty", wantSet: false},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
prepared, _ := preparedTerminalDebugPipeline(t)
|
||||||
|
output, err := New().Run(context.Background(), RunInput{
|
||||||
|
Prepared: prepared,
|
||||||
|
RawInput: []byte("input"),
|
||||||
|
SessionID: test.session,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
value, found := output.Manifest.Metadata["session_id"]
|
||||||
|
if found != test.wantSet {
|
||||||
|
t.Fatalf("manifest session presence = %t, want %t; metadata = %#v", found, test.wantSet, output.Manifest.Metadata)
|
||||||
|
}
|
||||||
|
if found && value != test.want {
|
||||||
|
t.Fatalf("manifest session = %#v, want %q", value, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user