Adapt corrections through PromptKit

This commit is contained in:
2026-08-26 23:50:24 +00:00
parent 85a5b52be7
commit 1a7b20c766
8 changed files with 267 additions and 49 deletions

View File

@@ -0,0 +1,84 @@
package contracts
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
// DebugStructuredCompletionRequest is the content-safe representation of a
// structured completion request for ordinary diagnostics and summaries.
// Detailed prompt material remains available only through the explicitly
// requested LLM debug trace.
type DebugStructuredCompletionRequest struct {
StageName string `json:"stage_name,omitempty"`
PromptID string `json:"prompt_id,omitempty"`
PromptVersion string `json:"prompt_version,omitempty"`
ProfileID string `json:"profile_id,omitempty"`
SessionID string `json:"session_id,omitempty"`
InputCount int `json:"input_count"`
VariableCount int `json:"variable_count"`
StructuredOutputRepairAttempts *int `json:"structured_output_repair_attempts,omitempty"`
Correction *DebugSemanticCorrection `json:"correction,omitempty"`
}
// DebugSemanticCorrection records only safe correction metadata. It never
// exposes the assistant response or user guidance text.
type DebugSemanticCorrection struct {
AssistantResponseBytes int `json:"assistant_response_bytes"`
AssistantResponseDigest string `json:"assistant_response_digest"`
UserGuidanceBytes int `json:"user_guidance_bytes"`
UserGuidanceDigest string `json:"user_guidance_digest"`
}
// DebugSummary returns a content-safe representation suitable for ordinary
// diagnostics. It does not validate or retain correction content.
func (request StructuredCompletionRequest) DebugSummary() DebugStructuredCompletionRequest {
summary := DebugStructuredCompletionRequest{
StageName: request.StageName,
PromptID: request.PromptID,
PromptVersion: request.PromptVersion,
ProfileID: request.ProfileID,
SessionID: request.SessionID,
InputCount: len(request.Inputs),
VariableCount: len(request.Vars),
}
if request.StructuredOutputRepairAttempts != nil {
attempts := *request.StructuredOutputRepairAttempts
summary.StructuredOutputRepairAttempts = &attempts
}
if request.Correction != nil {
summary.Correction = request.Correction.DebugSummary()
}
return summary
}
// DebugSummary returns content-safe correction metadata suitable for ordinary
// diagnostics.
func (correction *SemanticCorrection) DebugSummary() *DebugSemanticCorrection {
if correction == nil {
return nil
}
return &DebugSemanticCorrection{
AssistantResponseBytes: len(correction.AssistantResponse),
AssistantResponseDigest: debugContentDigest(correction.AssistantResponse),
UserGuidanceBytes: len(correction.UserGuidance),
UserGuidanceDigest: debugContentDigest([]byte(correction.UserGuidance)),
}
}
// String prevents ordinary request formatting from exposing correction
// content. Use the explicitly requested debug trace for complete messages.
func (request StructuredCompletionRequest) String() string {
return fmt.Sprintf("%+v", request.DebugSummary())
}
// GoString gives %#v formatting the same content-safe behavior as String.
func (request StructuredCompletionRequest) GoString() string {
return request.String()
}
func debugContentDigest(content []byte) string {
sum := sha256.Sum256(content)
return "sha256:" + hex.EncodeToString(sum[:])
}

View File

@@ -3,6 +3,7 @@ package contracts
import (
"bytes"
"encoding/json"
"fmt"
"strings"
"testing"
)
@@ -139,3 +140,37 @@ func TestCloneStructuredCompletionRequestOwnsCorrection(t *testing.T) {
t.Fatalf("cloned repair attempts = %v, want 2", clone.StructuredOutputRepairAttempts)
}
}
func TestStructuredCompletionRequestDebugSummaryOmitsCorrectionContent(t *testing.T) {
const assistantResponse = `{"secret":"assistant response"}`
const userGuidance = "secret user guidance"
correction, err := NewSemanticCorrection([]byte(assistantResponse), userGuidance)
if err != nil {
t.Fatalf("NewSemanticCorrection() error = %v", err)
}
request := StructuredCompletionRequest{
Inputs: LLMInputSet{"source": NewLLMInputMaterial("source", "application/json", []byte(`{"source":true}`), "", "")},
Vars: map[string]any{"custom": "value"},
Correction: correction,
}
summary := request.DebugSummary()
if summary.InputCount != 1 || summary.VariableCount != 1 || summary.Correction == nil {
t.Fatalf("debug summary = %#v, want input, variable, and correction metadata", summary)
}
if summary.Correction.AssistantResponseBytes != len(assistantResponse) || summary.Correction.UserGuidanceBytes != len(userGuidance) ||
summary.Correction.AssistantResponseDigest == "" || summary.Correction.UserGuidanceDigest == "" {
t.Fatalf("correction summary = %#v, want byte counts and digests", summary.Correction)
}
encoded, err := json.Marshal(summary)
if err != nil {
t.Fatalf("marshal debug summary: %v", err)
}
for _, rendered := range []string{string(encoded), fmt.Sprintf("%+v", request), fmt.Sprintf("%#v", request)} {
for _, secret := range []string{assistantResponse, userGuidance} {
if strings.Contains(rendered, secret) {
t.Fatalf("content-safe request rendering leaked %q: %s", secret, rendered)
}
}
}
}