Forward sessions through PromptKit requests
This commit is contained in:
@@ -15,7 +15,7 @@ Notarius relies on the root `promptkit` package to:
|
||||
- construct an `Engine` with filesystem-backed prompt, schema, and optional
|
||||
profile sources;
|
||||
- prepare and run a `RunRequest` with named inline artifacts, variables,
|
||||
prompt identity, and profile selection;
|
||||
a direct session ID, prompt identity, and profile selection;
|
||||
- return rendered debug material, validated structured output, selected
|
||||
profile and model metadata, and token usage;
|
||||
- distinguish structured-output validation failure from execution failure; and
|
||||
@@ -27,6 +27,12 @@ adapter boundary. It also retains responsibility for pipeline retries,
|
||||
scheduling, debug persistence, redaction, profile provenance, and conversion
|
||||
from private model responses into durable domain artifacts.
|
||||
|
||||
Notarius sends its trimmed run session through PromptKit's direct session
|
||||
field, which is authoritative for provider session behavior. It also retains
|
||||
the same value as the `session_id` prompt variable for maintained prompt
|
||||
compatibility. Session IDs are stable, non-secret correlation identifiers and
|
||||
may be exposed to providers and provider observability.
|
||||
|
||||
## Notarius Ownership
|
||||
|
||||
[LLM Runtime Internals](../internal/llm.md) describes how Notarius mounts
|
||||
|
||||
@@ -26,12 +26,15 @@ durable schemas. Those responsibilities remain with the module and its
|
||||
|
||||
`PromptKitClient` validates the request target and prompt identity, maps each
|
||||
named material to a PromptKit inline artifact while preserving its origin URI,
|
||||
maps the request session to the existing `session_id` prompt variable, forwards
|
||||
profile selection, then prepares and runs the prompt. PromptKit v0.1.0 has no
|
||||
direct request-level session field. The adapter returns PromptKit’s validated
|
||||
raw bytes rather than re-encoding the decoded target. An empty optional
|
||||
material is represented as one space so its named input is retained by
|
||||
PromptKit.
|
||||
maps the trimmed request session to PromptKit's direct per-run session field,
|
||||
retains the same value as the `session_id` prompt variable for maintained
|
||||
prompt compatibility, forwards profile selection, then prepares and runs the
|
||||
prompt. The direct field is authoritative for provider session behavior. A
|
||||
session ID is a stable, non-secret correlation identifier and may be exposed
|
||||
to providers and provider observability. The adapter returns PromptKit’s
|
||||
validated raw bytes rather than re-encoding the decoded target. An empty
|
||||
optional material is represented as one space so its named input is retained
|
||||
by PromptKit.
|
||||
|
||||
An empty request profile lets the prompt select its configured default. The CLI
|
||||
prepares every explicitly selected binding profile before a run begins, so a
|
||||
@@ -136,7 +139,7 @@ does not install another timeout wrapper around PromptKit.
|
||||
|
||||
The selected PromptKit profile owns generation settings. Notarius binding
|
||||
retries remain outside the adapter and repeat the complete module operation
|
||||
and validation chain. PromptKit v0.1.0 does not add a provider retry loop.
|
||||
and validation chain. PromptKit does not add a provider retry loop.
|
||||
Operator-facing behavior is summarized in
|
||||
[Operations](../operations.md#operational-limits), and the pinned upstream
|
||||
contract is identified in
|
||||
|
||||
@@ -91,31 +91,6 @@ safety checks, and deterministic application of accepted changes.
|
||||
- Add media-type validators when non-JSON artifact representations are
|
||||
introduced.
|
||||
|
||||
## LLM Runtime Evolution
|
||||
|
||||
### Native Session Propagation
|
||||
|
||||
- Once upstream PromptKit exposes a direct request-level session identifier,
|
||||
propagate the existing `StructuredCompletionRequest.SessionID` through the
|
||||
PromptKit adapter's native session field.
|
||||
- Preserve the current `--session-id` invocation contract and its run-wide
|
||||
propagation to every prompt-facing module and validator. Do not introduce a
|
||||
second session configuration surface.
|
||||
- Retain session identity in checkpoint provenance so runs with different
|
||||
sessions cannot reuse one another's LLM-derived checkpoints.
|
||||
- Define the upstream compatibility and prompt-variable transition explicitly:
|
||||
native provider session behavior must not silently remove a `session_id`
|
||||
prompt variable while maintained prompts still consume it.
|
||||
- Add adapter and assembled-run coverage for exact forwarding, trimming,
|
||||
concurrent-run isolation, and unsupported-provider behavior once the
|
||||
upstream contract is available.
|
||||
|
||||
This work is blocked because PromptKit v0.1.0 does not expose the required
|
||||
direct request-level session field. Notarius already carries a run-scoped
|
||||
session ID through its CLI, pipeline requests, checkpoint identity, and a
|
||||
`session_id` prompt variable; that prompt-variable propagation is not native
|
||||
provider session support.
|
||||
|
||||
## Further Reference Evolution
|
||||
|
||||
- Make prior-run artifacts easier to bind as references without changing the
|
||||
|
||||
@@ -93,13 +93,15 @@ func (c *PromptKitClient) CompleteStructured(ctx context.Context, req contracts.
|
||||
if promptID == "" {
|
||||
return contracts.StructuredCompletionResponse{}, fmt.Errorf("structured completion prompt_id must not be empty")
|
||||
}
|
||||
sessionID := strings.TrimSpace(req.SessionID)
|
||||
|
||||
runReq := promptkit.RunRequest{
|
||||
PromptID: promptID,
|
||||
PromptVersion: strings.TrimSpace(req.PromptVersion),
|
||||
ProfileID: strings.TrimSpace(req.ProfileID),
|
||||
SessionID: sessionID,
|
||||
Inputs: promptKitInputs(req.Inputs),
|
||||
Vars: promptKitVars(req),
|
||||
Vars: promptKitVars(req, sessionID),
|
||||
}
|
||||
prepared, err := c.engine.Prepare(ctx, runReq)
|
||||
if err != nil {
|
||||
@@ -342,7 +344,7 @@ func promptKitInputs(inputs contracts.LLMInputSet) map[string]promptkit.Artifact
|
||||
return out
|
||||
}
|
||||
|
||||
func promptKitVars(req contracts.StructuredCompletionRequest) map[string]string {
|
||||
func promptKitVars(req contracts.StructuredCompletionRequest, sessionID string) map[string]string {
|
||||
vars := make(map[string]string, len(req.Vars)+1)
|
||||
for key, value := range req.Vars {
|
||||
name := strings.TrimSpace(key)
|
||||
@@ -351,7 +353,7 @@ func promptKitVars(req contracts.StructuredCompletionRequest) map[string]string
|
||||
}
|
||||
vars[name] = fmt.Sprint(value)
|
||||
}
|
||||
if sessionID := strings.TrimSpace(req.SessionID); sessionID != "" {
|
||||
if sessionID != "" {
|
||||
vars["session_id"] = sessionID
|
||||
}
|
||||
if len(vars) == 0 {
|
||||
|
||||
@@ -28,10 +28,10 @@ func TestPromptKitClientMapsPromptRequestAndUnmarshalsOutput(t *testing.T) {
|
||||
}
|
||||
resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
|
||||
StageName: "test-stage",
|
||||
PromptID: "adapter.test",
|
||||
PromptID: "adapter.direct-session",
|
||||
PromptVersion: "v1",
|
||||
ProfileID: "explicit-profile",
|
||||
SessionID: "session-123",
|
||||
SessionID: " session-123 ",
|
||||
Inputs: contracts.LLMInputSet{
|
||||
"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "sha256:source", "file:///source.json"),
|
||||
},
|
||||
@@ -52,8 +52,10 @@ func TestPromptKitClientMapsPromptRequestAndUnmarshalsOutput(t *testing.T) {
|
||||
if resp.Debug == nil || resp.Debug.Prompt == nil {
|
||||
t.Fatalf("debug prompt = nil, want prepared prompt material")
|
||||
}
|
||||
if resp.Debug.Prompt.PromptID != "adapter.test" || resp.Debug.Prompt.SelectedProfileID != "explicit-profile" {
|
||||
t.Fatalf("debug prompt metadata = %#v, want prompt/profile", resp.Debug.Prompt)
|
||||
if resp.Debug.Prompt.PromptID != "adapter.direct-session" ||
|
||||
resp.Debug.Prompt.SelectedProfileID != "explicit-profile" ||
|
||||
resp.Debug.Prompt.SessionID != "session-123" {
|
||||
t.Fatalf("debug prompt metadata = %#v, want prompt/profile/session", resp.Debug.Prompt)
|
||||
}
|
||||
if len(resp.Debug.Prompt.Messages) != 1 || !strings.Contains(resp.Debug.Prompt.Messages[0].Content, `{"source":true}`) {
|
||||
t.Fatalf("debug prompt messages = %#v, want rendered input content", resp.Debug.Prompt.Messages)
|
||||
@@ -95,6 +97,59 @@ func TestPromptKitClientMapsPromptRequestAndUnmarshalsOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitClientRetainsSessionPromptVariable(t *testing.T) {
|
||||
fake := &fakePromptKitLLM{content: `{"ok":true}`}
|
||||
client := newTestPromptKitClient(t, fake)
|
||||
|
||||
var out map[string]any
|
||||
_, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
|
||||
PromptID: "adapter.test",
|
||||
SessionID: " canonical-session ",
|
||||
Inputs: contracts.LLMInputSet{
|
||||
"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
|
||||
},
|
||||
Vars: map[string]any{
|
||||
"custom": "value",
|
||||
"session_id": "caller-session",
|
||||
},
|
||||
}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("CompleteStructured() error = %v, want nil", err)
|
||||
}
|
||||
gotReq := fake.lastRequest()
|
||||
if gotReq.Prompt.SessionID != "canonical-session" {
|
||||
t.Fatalf("session id = %q, want canonical-session", gotReq.Prompt.SessionID)
|
||||
}
|
||||
if len(gotReq.Prompt.Messages) != 1 ||
|
||||
!strings.Contains(gotReq.Prompt.Messages[0].Content, "Session: canonical-session") ||
|
||||
strings.Contains(gotReq.Prompt.Messages[0].Content, "caller-session") {
|
||||
t.Fatalf("rendered messages = %#v, want canonical session compatibility variable", gotReq.Prompt.Messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitClientDoesNotInventDirectSession(t *testing.T) {
|
||||
fake := &fakePromptKitLLM{content: `{"ok":true}`}
|
||||
client := newTestPromptKitClient(t, fake)
|
||||
|
||||
var out map[string]any
|
||||
resp, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
|
||||
PromptID: "adapter.direct-session",
|
||||
Inputs: contracts.LLMInputSet{
|
||||
"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
|
||||
},
|
||||
Vars: map[string]any{"custom": "value"},
|
||||
}, &out)
|
||||
if err != nil {
|
||||
t.Fatalf("CompleteStructured() error = %v, want nil", err)
|
||||
}
|
||||
if got := fake.lastRequest().Prompt.SessionID; got != "" {
|
||||
t.Fatalf("session id = %q, want empty", got)
|
||||
}
|
||||
if resp.Debug == nil || resp.Debug.Prompt == nil || resp.Debug.Prompt.SessionID != "" {
|
||||
t.Fatalf("debug prompt = %#v, want no effective session", resp.Debug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPromptKitClientReportsAssetAndEngineConstructionFailures(t *testing.T) {
|
||||
t.Run("assets", func(t *testing.T) {
|
||||
registry := NewAssetRegistry()
|
||||
@@ -443,6 +498,22 @@ func newTestPromptKitAssets(t *testing.T) *AssetRegistry {
|
||||
version: "v1"
|
||||
default_profile: default-profile
|
||||
session_id: "{{ .session_id }}"
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
content_type: application/json
|
||||
messages:
|
||||
- role: user
|
||||
content: "Transcript: {{ input \"transcript\" }} Custom: {{ index . \"custom\" }} Session: {{ .session_id }}"
|
||||
output:
|
||||
format: json
|
||||
validation_mode: json_schema
|
||||
schema_path: adapter.schema.json
|
||||
repair_attempts: 0
|
||||
`)},
|
||||
"adapter.direct-session.yaml": {Data: []byte(`id: adapter.direct-session
|
||||
version: "v1"
|
||||
default_profile: default-profile
|
||||
inputs:
|
||||
- name: transcript
|
||||
required: true
|
||||
|
||||
Reference in New Issue
Block a user