Add direct per-run session overrides
This commit is contained in:
@@ -16,6 +16,7 @@ func toDomainRunRequest(req RunRequest) (domain.RunRequest, error) {
|
||||
PromptID: req.PromptID,
|
||||
PromptVersion: req.PromptVersion,
|
||||
ProfileID: req.ProfileID,
|
||||
SessionID: req.SessionID,
|
||||
APIKey: req.APIKey,
|
||||
Inputs: toDomainArtifactRefMap(req.Inputs),
|
||||
Vars: copyStringMap(req.Vars),
|
||||
@@ -59,6 +60,7 @@ func fromDomainRunResult(result *domain.RunResult) *RunResult {
|
||||
PromptID: result.PromptID,
|
||||
PromptVersion: result.PromptVersion,
|
||||
PromptHash: result.PromptHash,
|
||||
SessionID: result.SessionID,
|
||||
RenderedPromptHash: result.RenderedPromptHash,
|
||||
SelectedProfileID: result.SelectedProfileID,
|
||||
SelectedBackendID: result.SelectedBackendID,
|
||||
|
||||
@@ -369,7 +369,7 @@ resolved string.
|
||||
|
||||
## Stage 2 — Direct Session Resolution And Result Metadata
|
||||
|
||||
**Status:** Pending.
|
||||
**Status:** Complete.
|
||||
|
||||
### Goal
|
||||
|
||||
|
||||
@@ -611,19 +611,26 @@ func TestEngineRunWithDirectorySourcesAndFileInputs(t *testing.T) {
|
||||
|
||||
func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||
const directKey = "direct-injected-key"
|
||||
const directSession = "assembled-session"
|
||||
fake := &fakeLLMClient{
|
||||
response: &promptkit.GenerateResponse{Content: `{"events":[{"title":"Archive labelled"}]}`},
|
||||
}
|
||||
engine := newContractEngineWithOptions(t, frameworkSchemaDir, promptkit.WithLLMClient(fake))
|
||||
|
||||
_, err := engine.Run(context.Background(), promptkit.RunRequest{
|
||||
PromptID: frameworkStructuredEventsPromptID,
|
||||
APIKey: directKey,
|
||||
runRequest := promptkit.RunRequest{
|
||||
PromptID: frameworkStructuredEventsPromptID,
|
||||
SessionID: " " + directSession + " ",
|
||||
APIKey: directKey,
|
||||
Inputs: map[string]promptkit.ArtifactRef{
|
||||
"transcript": promptkit.Inline("Rin opens the gate."),
|
||||
"glossary": promptkit.Inline("gate: A guarded passage."),
|
||||
},
|
||||
})
|
||||
}
|
||||
prepared, err := engine.Prepare(context.Background(), runRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("expected prepare to succeed, got %v", err)
|
||||
}
|
||||
result, err := engine.Run(context.Background(), runRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("expected run to succeed, got %v", err)
|
||||
}
|
||||
@@ -634,6 +641,16 @@ func TestRunPassesPreparedRequestToInjectedLLMClient(t *testing.T) {
|
||||
if len(req.Prompt.Messages) != 2 || !strings.Contains(req.Prompt.Messages[1].Content, "Rin opens the gate.") {
|
||||
t.Fatalf("expected rendered prompt in generate request, got %+v", req.Prompt)
|
||||
}
|
||||
if prepared.SessionID != directSession ||
|
||||
req.Prompt.SessionID != directSession ||
|
||||
result.SessionID != directSession {
|
||||
t.Fatalf(
|
||||
"direct session did not propagate consistently: prepared=%q generated=%q result=%q",
|
||||
prepared.SessionID,
|
||||
req.Prompt.SessionID,
|
||||
result.SessionID,
|
||||
)
|
||||
}
|
||||
if req.StructuredOutput == nil || req.StructuredOutput.Type != promptkit.StructuredOutputJSONSchema || req.StructuredOutput.JSONSchema == nil {
|
||||
t.Fatalf("expected structured output handoff, got %+v", req.StructuredOutput)
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ type RunRequest struct {
|
||||
PromptID string
|
||||
PromptVersion string
|
||||
ProfileID string
|
||||
SessionID string
|
||||
APIKey string `json:"-" yaml:"-"`
|
||||
Inputs map[string]ArtifactRef
|
||||
Vars map[string]string
|
||||
@@ -79,6 +80,7 @@ type RunResult struct {
|
||||
PromptID string
|
||||
PromptVersion string
|
||||
PromptHash string
|
||||
SessionID string
|
||||
RenderedPromptHash string
|
||||
SelectedProfileID string
|
||||
SelectedBackendID string
|
||||
|
||||
19
internal/domain/session.go
Normal file
19
internal/domain/session.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// NormalizeSessionID applies the shared session identifier rule.
|
||||
func NormalizeSessionID(raw string) (string, error) {
|
||||
normalized := strings.TrimSpace(raw)
|
||||
if normalized == "" {
|
||||
return "", nil
|
||||
}
|
||||
if length := utf8.RuneCountInString(normalized); length > SessionIDMaxLength {
|
||||
return "", fmt.Errorf("session_id length %d exceeds maximum %d", length, SessionIDMaxLength)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
57
internal/domain/session_test.go
Normal file
57
internal/domain/session_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNormalizeSessionID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "trims surrounding Unicode whitespace",
|
||||
raw: "\u2003 session-123 \u2003",
|
||||
want: "session-123",
|
||||
},
|
||||
{
|
||||
name: "blank input is omitted",
|
||||
raw: " \t\u2003 ",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "maximum Unicode length is accepted",
|
||||
raw: strings.Repeat("界", SessionIDMaxLength),
|
||||
want: strings.Repeat("界", SessionIDMaxLength),
|
||||
},
|
||||
{
|
||||
name: "one Unicode code point over maximum is rejected",
|
||||
raw: strings.Repeat("界", SessionIDMaxLength+1),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NormalizeSessionID(tt.raw)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("expected normalization error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeds maximum") {
|
||||
t.Fatalf("expected useful length diagnostic, got %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("normalize session id: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("normalized session id = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/defaults"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
@@ -177,12 +176,11 @@ func openAIChatRequestFromGenerateRequest(req domain.GenerateRequest, defaultMod
|
||||
wireReq := openAIChatRequest{
|
||||
Model: model,
|
||||
}
|
||||
if sessionID := strings.TrimSpace(req.Prompt.SessionID); sessionID != "" {
|
||||
if n := utf8.RuneCountInString(sessionID); n > domain.SessionIDMaxLength {
|
||||
return openAIChatRequest{}, fmt.Errorf("session_id length %d exceeds maximum %d", n, domain.SessionIDMaxLength)
|
||||
}
|
||||
wireReq.SessionID = sessionID
|
||||
sessionID, err := domain.NormalizeSessionID(req.Prompt.SessionID)
|
||||
if err != nil {
|
||||
return openAIChatRequest{}, err
|
||||
}
|
||||
wireReq.SessionID = sessionID
|
||||
|
||||
wireReq.Messages = make([]openAIChatRequestMessage, 0, len(req.Prompt.Messages))
|
||||
for _, msg := range req.Prompt.Messages {
|
||||
|
||||
@@ -5,10 +5,9 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode/utf8"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -95,10 +94,6 @@ func (r *goRenderer) Render(ctx context.Context, definition *domain.PromptDefini
|
||||
}
|
||||
|
||||
func renderSessionID(raw string, funcs template.FuncMap, vars map[string]string) (string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
tmpl, err := template.New("session_id").Funcs(funcs).Option("missingkey=error").Parse(raw)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: session_id: %v", ErrInvalidTemplate, err)
|
||||
@@ -109,9 +104,9 @@ func renderSessionID(raw string, funcs template.FuncMap, vars map[string]string)
|
||||
return "", fmt.Errorf("%w: session_id: %w", ErrRenderFailure, err)
|
||||
}
|
||||
|
||||
sessionID := strings.TrimSpace(buf.String())
|
||||
if n := utf8.RuneCountInString(sessionID); n > domain.SessionIDMaxLength {
|
||||
return "", fmt.Errorf("%w: session_id length %d exceeds maximum %d", ErrRenderFailure, n, domain.SessionIDMaxLength)
|
||||
sessionID, err := domain.NormalizeSessionID(buf.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: session_id: %v", ErrRenderFailure, err)
|
||||
}
|
||||
return sessionID, nil
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (*domain.RunRes
|
||||
PromptID: prepared.PromptID,
|
||||
PromptVersion: prepared.PromptVersion,
|
||||
PromptHash: prepared.PromptHash,
|
||||
SessionID: prepared.SessionID,
|
||||
RenderedPromptHash: prepared.RenderedPromptHash,
|
||||
SelectedProfileID: prepared.SelectedProfileID,
|
||||
SelectedBackendID: prepared.SelectedBackendID,
|
||||
@@ -178,6 +179,10 @@ func (r *Runner) Prepare(ctx context.Context, req domain.RunRequest) (*domain.Pr
|
||||
if strings.TrimSpace(req.PromptID) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidRequest)
|
||||
}
|
||||
directSessionID, err := domain.NormalizeSessionID(req.SessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
|
||||
}
|
||||
|
||||
start := time.Now().UTC()
|
||||
|
||||
@@ -251,10 +256,19 @@ func (r *Runner) Prepare(ctx context.Context, req domain.RunRequest) (*domain.Pr
|
||||
inputHashes[name] = art.Hash
|
||||
}
|
||||
|
||||
renderedPrompt, err := r.renderer.Render(ctx, def, resolvedInputs, req.Vars)
|
||||
definitionToRender := def
|
||||
if directSessionID != "" {
|
||||
definitionCopy := *def
|
||||
definitionCopy.SessionID = ""
|
||||
definitionToRender = &definitionCopy
|
||||
}
|
||||
renderedPrompt, err := r.renderer.Render(ctx, definitionToRender, resolvedInputs, req.Vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptRender, err)
|
||||
}
|
||||
if directSessionID != "" {
|
||||
renderedPrompt.SessionID = directSessionID
|
||||
}
|
||||
|
||||
end := time.Now().UTC()
|
||||
return &domain.PreparedRun{
|
||||
|
||||
@@ -222,6 +222,145 @@ func TestRunnerPrepareWithExplicitProfileSelection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerDirectSessionResolution(t *testing.T) {
|
||||
t.Run("direct value wins and changes only the rendered prompt hash", func(t *testing.T) {
|
||||
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
||||
def.SessionID = "template-{{.template_session}}"
|
||||
promptRepo := &fakePromptRepo{def: def}
|
||||
runner := NewRunner(
|
||||
promptRepo,
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
nil,
|
||||
defaultArtifactReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
&fakeLLM{forbid: true},
|
||||
nil,
|
||||
)
|
||||
req := domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
Inputs: singleInputRef(),
|
||||
Vars: map[string]string{"template_session": "from-template"},
|
||||
}
|
||||
|
||||
req.SessionID = " direct-one "
|
||||
first, err := runner.Prepare(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare first direct session: %v", err)
|
||||
}
|
||||
req.SessionID = "direct-two"
|
||||
second, err := runner.Prepare(context.Background(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare second direct session: %v", err)
|
||||
}
|
||||
|
||||
if first.SessionID != "direct-one" || second.SessionID != "direct-two" {
|
||||
t.Fatalf("direct sessions were not normalized: first=%q second=%q", first.SessionID, second.SessionID)
|
||||
}
|
||||
if first.PromptHash != second.PromptHash {
|
||||
t.Fatalf("direct session changed prompt-definition hash: first=%q second=%q", first.PromptHash, second.PromptHash)
|
||||
}
|
||||
if first.RenderedPromptHash == second.RenderedPromptHash {
|
||||
t.Fatal("changing direct session did not change rendered-prompt hash")
|
||||
}
|
||||
if def.SessionID != "template-{{.template_session}}" {
|
||||
t.Fatalf("repository-owned prompt definition was mutated: %q", def.SessionID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("direct value bypasses failing session template without changing messages", func(t *testing.T) {
|
||||
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
||||
def.SessionID = "{{.missing_session}}"
|
||||
def.Templates = []domain.PromptMessageTemplate{
|
||||
{Role: "user", Content: "Hello {{.name}}"},
|
||||
}
|
||||
runner := NewRunner(
|
||||
&fakePromptRepo{def: def},
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
nil,
|
||||
defaultArtifactReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
&fakeLLM{forbid: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
SessionID: "direct-session",
|
||||
Inputs: singleInputRef(),
|
||||
Vars: map[string]string{"name": "Rin"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("prepare with direct session: %v", err)
|
||||
}
|
||||
if prepared.SessionID != "direct-session" {
|
||||
t.Fatalf("prepared session id = %q, want direct-session", prepared.SessionID)
|
||||
}
|
||||
if len(prepared.Messages) != 1 || prepared.Messages[0].Content != "Hello Rin" {
|
||||
t.Fatalf("message templates did not render normally: %+v", prepared.Messages)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("blank direct value retains prompt template behavior", func(t *testing.T) {
|
||||
def := promptDef(domain.FormatText, domain.ValidationNone, 0)
|
||||
def.SessionID = " template-{{.template_session}} "
|
||||
runner := NewRunner(
|
||||
&fakePromptRepo{def: def},
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
nil,
|
||||
defaultArtifactReader(),
|
||||
prompt.NewGoRenderer(),
|
||||
&fakeLLM{forbid: true},
|
||||
nil,
|
||||
)
|
||||
|
||||
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
SessionID: " \t ",
|
||||
Inputs: singleInputRef(),
|
||||
Vars: map[string]string{"template_session": "rendered"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("prepare with prompt session template: %v", err)
|
||||
}
|
||||
if prepared.SessionID != "template-rendered" {
|
||||
t.Fatalf("prepared session id = %q, want template-rendered", prepared.SessionID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("overlong direct value fails before loading or generation", func(t *testing.T) {
|
||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
||||
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "unexpected"}}
|
||||
runner := NewRunner(
|
||||
promptRepo,
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
nil,
|
||||
defaultArtifactReader(),
|
||||
defaultRenderer(),
|
||||
llmClient,
|
||||
nil,
|
||||
)
|
||||
|
||||
_, err := runner.Run(context.Background(), domain.RunRequest{
|
||||
PromptID: "p",
|
||||
ProfileID: "exec",
|
||||
SessionID: strings.Repeat("界", domain.SessionIDMaxLength+1),
|
||||
Inputs: singleInputRef(),
|
||||
})
|
||||
if !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("expected ErrInvalidRequest, got %v", err)
|
||||
}
|
||||
if promptRepo.lastID != "" {
|
||||
t.Fatalf("invalid direct session loaded prompt %q", promptRepo.lastID)
|
||||
}
|
||||
if llmClient.calls != 0 {
|
||||
t.Fatalf("invalid direct session invoked generation %d times", llmClient.calls)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerPrepareUsesPromptDefaultProfileWhenNoExplicitProfileID(t *testing.T) {
|
||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
||||
promptRepo.def.DefaultProfile = "from-prompt"
|
||||
@@ -900,6 +1039,9 @@ func TestRunnerRunSuccessful(t *testing.T) {
|
||||
if llmClient.lastReq.Prompt.SessionID != "session-123" {
|
||||
t.Fatalf("expected session id to be sent to llm, got %q", llmClient.lastReq.Prompt.SessionID)
|
||||
}
|
||||
if res.SessionID != "session-123" {
|
||||
t.Fatalf("expected session id in run result, got %q", res.SessionID)
|
||||
}
|
||||
if res.Usage.TotalTokens != 7 {
|
||||
t.Fatalf("expected token usage to be retained, got %+v", res.Usage)
|
||||
}
|
||||
|
||||
3
json.go
3
json.go
@@ -81,6 +81,7 @@ func (r RunResult) MarshalJSON() ([]byte, error) {
|
||||
PromptID: r.PromptID,
|
||||
PromptVersion: r.PromptVersion,
|
||||
PromptHash: r.PromptHash,
|
||||
SessionID: r.SessionID,
|
||||
RenderedPromptHash: r.RenderedPromptHash,
|
||||
SelectedProfileID: r.SelectedProfileID,
|
||||
SelectedBackendID: r.SelectedBackendID,
|
||||
@@ -111,6 +112,7 @@ func (r *RunResult) UnmarshalJSON(data []byte) error {
|
||||
PromptID: wire.PromptID,
|
||||
PromptVersion: wire.PromptVersion,
|
||||
PromptHash: wire.PromptHash,
|
||||
SessionID: wire.SessionID,
|
||||
RenderedPromptHash: wire.RenderedPromptHash,
|
||||
SelectedProfileID: wire.SelectedProfileID,
|
||||
SelectedBackendID: wire.SelectedBackendID,
|
||||
@@ -138,6 +140,7 @@ type runResultJSON struct {
|
||||
PromptID string `json:"prompt_id"`
|
||||
PromptVersion string `json:"prompt_version,omitempty"`
|
||||
PromptHash string `json:"prompt_hash,omitempty"`
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
RenderedPromptHash string `json:"rendered_prompt_hash"`
|
||||
SelectedProfileID string `json:"selected_profile_id"`
|
||||
SelectedBackendID string `json:"selected_backend_id,omitempty"`
|
||||
|
||||
@@ -406,6 +406,7 @@ func TestRunResultJSONUsesMillisecondsAndRoundTrips(t *testing.T) {
|
||||
result := promptkit.RunResult{
|
||||
RunID: "opaque-run-id",
|
||||
Artifact: promptkit.Artifact{Name: "output", ContentType: "text/plain", Body: []byte("ok")},
|
||||
SessionID: "session-123",
|
||||
StartTime: start,
|
||||
EndTime: start.Add(1500 * time.Millisecond),
|
||||
Duration: 1500 * time.Millisecond,
|
||||
@@ -425,6 +426,9 @@ func TestRunResultJSONUsesMillisecondsAndRoundTrips(t *testing.T) {
|
||||
if _, exists := object["duration"]; exists {
|
||||
t.Fatalf("unexpected nanosecond duration field in %s", payload)
|
||||
}
|
||||
if got := object["session_id"]; got != result.SessionID {
|
||||
t.Fatalf("expected session_id=%q, got %#v in %s", result.SessionID, got, payload)
|
||||
}
|
||||
artifact, ok := object["artifact"].(map[string]any)
|
||||
if !ok || artifact["content_type"] != "text/plain" {
|
||||
t.Fatalf("expected stable artifact JSON fields, got %#v", object["artifact"])
|
||||
@@ -434,7 +438,10 @@ func TestRunResultJSONUsesMillisecondsAndRoundTrips(t *testing.T) {
|
||||
if err := json.Unmarshal(payload, &decoded); err != nil {
|
||||
t.Fatalf("unmarshal run result: %v", err)
|
||||
}
|
||||
if decoded.Duration != result.Duration || !decoded.StartTime.Equal(result.StartTime) || !decoded.EndTime.Equal(result.EndTime) {
|
||||
if decoded.SessionID != result.SessionID ||
|
||||
decoded.Duration != result.Duration ||
|
||||
!decoded.StartTime.Equal(result.StartTime) ||
|
||||
!decoded.EndTime.Equal(result.EndTime) {
|
||||
t.Fatalf("timing values did not round trip: got %#v, want %#v", decoded, result)
|
||||
}
|
||||
|
||||
@@ -442,11 +449,18 @@ func TestRunResultJSONUsesMillisecondsAndRoundTrips(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("marshal zero run result: %v", err)
|
||||
}
|
||||
for _, field := range []string{"start_time", "end_time", "duration_ms"} {
|
||||
for _, field := range []string{"session_id", "start_time", "end_time", "duration_ms"} {
|
||||
if strings.Contains(string(payload), `"`+field+`"`) {
|
||||
t.Fatalf("expected zero %s to be omitted, got %s", field, payload)
|
||||
}
|
||||
}
|
||||
var decodedEmpty promptkit.RunResult
|
||||
if err := json.Unmarshal(payload, &decodedEmpty); err != nil {
|
||||
t.Fatalf("unmarshal run result without session_id: %v", err)
|
||||
}
|
||||
if decodedEmpty.SessionID != "" {
|
||||
t.Fatalf("expected absent session_id to decode empty, got %q", decodedEmpty.SessionID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEngineValidationIsSinglePass(t *testing.T) {
|
||||
|
||||
18
types.go
18
types.go
@@ -91,6 +91,15 @@ type RunRequest struct {
|
||||
// profile is used; if both are empty, the error matches ErrProfileRequired
|
||||
// and ErrInvalidRequest.
|
||||
ProfileID string
|
||||
// SessionID optionally supplies a direct per-run session identifier. A
|
||||
// nonblank value is trimmed and overrides the prompt definition's
|
||||
// session_id template. A blank value supplies no direct override. The
|
||||
// maximum is 256 Unicode code points after trimming. A direct value is
|
||||
// opaque consumer metadata, not a credential, and may be exposed in
|
||||
// prepared values, results, collaborator requests, provider requests, and
|
||||
// provider observability. Callers should use stable, non-sensitive
|
||||
// identifiers.
|
||||
SessionID string
|
||||
// APIKey is a request-scoped direct credential. It takes precedence over
|
||||
// APIKeyEnv, is passed to the selected LLMClient, and is never included in
|
||||
// prepared values, results, hashes, JSON, String, or GoString output.
|
||||
@@ -140,7 +149,7 @@ type PreparedRun struct {
|
||||
StructuredOutput *StructuredOutputSpec `json:"structured_output,omitempty"`
|
||||
// InputHashes maps every supplied input name to its opaque artifact hash.
|
||||
InputHashes map[string]string `json:"input_hashes,omitempty"`
|
||||
// SessionID is the trimmed rendered session identifier, if any.
|
||||
// SessionID is the effective direct or rendered session identifier, if any.
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
// RenderedPromptHash is an opaque equality value for SessionID and Messages.
|
||||
RenderedPromptHash string `json:"rendered_prompt_hash"`
|
||||
@@ -179,6 +188,9 @@ type RunResult struct {
|
||||
// PromptHash is the same opaque definition equality value exposed by
|
||||
// PreparedRun.
|
||||
PromptHash string `json:"prompt_hash,omitempty"`
|
||||
// SessionID is the effective direct or rendered session identifier, if any.
|
||||
// JSON omits an empty value.
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
// RenderedPromptHash is the same opaque rendered-prompt equality value
|
||||
// computed during preparation.
|
||||
RenderedPromptHash string `json:"rendered_prompt_hash"`
|
||||
@@ -497,8 +509,8 @@ type TokenUsage struct {
|
||||
// RenderedPrompt is the fully rendered prompt passed to an LLM client and has
|
||||
// a stable JSON representation.
|
||||
type RenderedPrompt struct {
|
||||
// SessionID is the optional trimmed session identifier rendered from the
|
||||
// prompt definition.
|
||||
// SessionID is the optional effective direct or rendered session
|
||||
// identifier supplied to the model client.
|
||||
SessionID string `json:"session_id,omitempty"`
|
||||
// Messages contains rendered messages in definition order.
|
||||
Messages []RenderedMessage `json:"messages"`
|
||||
|
||||
Reference in New Issue
Block a user