Carry proposal exchanges into repair replay

This commit is contained in:
2026-11-11 04:18:05 +00:00
parent eb45d0b623
commit 00848203d7
10 changed files with 267 additions and 41 deletions

View File

@@ -16,6 +16,8 @@ credential source names for artifact redaction without inspecting unrelated
environment values. See the [integration contract](../integrations/openai-compatible-llm.md)
for failure behavior and the [configuration reference](../config.md) for sources.
Proposal calls can retain the original prompt, effective variables, prepared messages, and final response for section-local repair replay. The adapter renders the repair definition with the selected profile, appends branch history to the original prompt, and checks the prepared prefix before execution. The requested repair definition is retained in call attribution alongside the native base-prompt metadata.
The observation collector tracks logical calls and execution events. Ordinary
reports use compact safe outcomes; request and response evidence is optional
debug material. The runner preserves ordered pipeline traversal and deterministic

View File

@@ -93,6 +93,8 @@ validator responses remains [deferred](future.md#domain-aware-response-recovery)
## Stage 3 — Carry the original exchange through proposal generation and the adapter
**Completed.**
1. Use `internal/framework/structuredoutput` for internal conversation data
shared by the role client and runner. Extend the existing structured request
with optional repair replay data. Keep the repair definition as the

View File

@@ -37,16 +37,17 @@ type StructuredCompletionResponse = structuredoutput.Response
// ModuleRunSpec identifies one resolved module instance in a pipeline.
type ModuleRunSpec struct {
ModuleKey string `json:"module_key"`
InstanceName string `json:"instance_name"`
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
Index int `json:"-"`
ID *string `json:"-"`
Settings config.Stage `json:"-"`
Glossary []glossary.Entry `json:"-"`
ProtectedTerms []glossary.Term `json:"-"`
ProposalClient StructuredLLMClient `json:"-"`
ValidationClient StructuredLLMClient `json:"-"`
ModuleKey string `json:"module_key"`
InstanceName string `json:"instance_name"`
ReplacementPolicy proposals.ReplacementPolicy `json:"replacement_policy"`
Index int `json:"-"`
ID *string `json:"-"`
Settings config.Stage `json:"-"`
Glossary []glossary.Entry `json:"-"`
ProtectedTerms []glossary.Term `json:"-"`
CaptureProposalExchange bool `json:"-"`
ProposalClient StructuredLLMClient `json:"-"`
ValidationClient StructuredLLMClient `json:"-"`
}
// SectionMetadata captures chunk-level metadata without embedding full segment payloads.
@@ -92,6 +93,7 @@ type ProposalRequest struct {
}
type ProposalResult struct {
Exchange *structuredoutput.Exchange `json:"-"`
Proposals []proposals.CorrectionProposal `json:"proposals,omitempty"`
Operations []outcomes.Operation `json:"operations,omitempty"`
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"reflect"
"sync"
"time"
@@ -72,6 +73,7 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
var record structuredoutput.CallRecord
if c.hooks.OnCall != nil || c.hooks.OnEventWithCall != nil {
record.CallID = c.runtime.nextCallID.Add(1)
record.RequestedPromptID = req.PromptID
record.Context = copyContext(req.Context)
record.Context.Role = c.role
}
@@ -106,8 +108,27 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
for name, value := range prompts.SharedVariables() {
vars[name] = value
}
baseID, baseVars := req.PromptID, vars
var suffix []pk.RenderedMessage
if req.Replay != nil {
if req.Replay.Exchange == nil {
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonSetupFailure, Message: "repair exchange is unavailable"}
}
instruction, renderErr := c.runtime.Engine.Prepare(ctx, pk.RunRequest{PromptID: req.PromptID, ProfileID: c.profileID, Vars: vars})
if renderErr != nil {
return response, classifiedFailure(parent, ctx, renderErr, nil)
}
response.RepairInstruction = convertMessages(instruction.Messages)
exchange := req.Replay.Exchange
baseID, baseVars = exchange.PromptID, exchange.Variables
suffix = append(suffix, pk.RenderedMessage{Role: pk.RoleAssistant, Content: exchange.FinalContent})
for _, message := range req.Replay.Turns {
suffix = append(suffix, pk.RenderedMessage{Role: message.Role, Content: message.Content})
}
suffix = append(suffix, instruction.Messages...)
}
handle, prepErr := c.runtime.Engine.PrepareExecution(ctx, pk.RunRequest{
PromptID: req.PromptID, ProfileID: c.profileID, Vars: vars,
PromptID: baseID, ProfileID: c.profileID, Vars: baseVars, AppendedMessages: suffix,
TransientRetries: &c.settings.TransientRetries,
Validation: contract,
Observer: func(event pk.ExecutionEvent) {
@@ -130,6 +151,12 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
}
defer handle.Discard()
details := handle.Details()
if req.Replay != nil {
prefix := req.Replay.Exchange.Messages
if len(details.Messages) < len(prefix) || !reflect.DeepEqual(convertMessages(details.Messages[:len(prefix)]), prefix) {
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonInternalFailure, Message: "repair conversation prefix changed"}
}
}
if name := details.EffectiveModelParams.APIKeyEnv; name != "" {
c.credentialMu.Lock()
if c.credentialNames == nil {
@@ -168,6 +195,13 @@ func (c *RoleClient) CompleteStructured(parent context.Context, req structuredou
}
return response, &structuredoutput.Failure{Class: structuredoutput.FailureFatal, Reason: outcomes.ReasonInternalFailure, Message: "validated structured output could not be decoded", Cause: decodeErr}
}
if req.CaptureExchange && req.Replay == nil {
frozen := make(map[string]string, len(vars))
for key, value := range vars {
frozen[key] = value
}
response.Exchange = &structuredoutput.Exchange{PromptID: req.PromptID, Variables: frozen, Messages: convertMessages(details.Messages), FinalContent: result.RawOutput}
}
response.Content = json.RawMessage(result.RawOutput)
response.Provider = "openai-compatible"
return response, nil
@@ -277,3 +311,11 @@ func (c *RoleClient) ConfiguredSecrets(ctx context.Context) []string {
c.credentialMu.Unlock()
return c.runtime.ConfiguredSecrets(ctx, names...)
}
func convertMessages(messages []pk.RenderedMessage) []structuredoutput.Message {
result := make([]structuredoutput.Message, len(messages))
for i, message := range messages {
result[i] = structuredoutput.Message{Role: string(message.Role), Content: message.Content}
}
return result
}

View File

@@ -3,6 +3,7 @@ package llm
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -11,9 +12,12 @@ import (
"reflect"
"strings"
"testing"
"time"
"gitea.maximumdirect.net/eric/audita/v3/internal/core/config"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/responseschema"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
"gitea.maximumdirect.net/eric/audita/v3/internal/prompts"
ck "go.maximumdirect.net/configkit"
pk "go.maximumdirect.net/promptkit"
)
@@ -148,3 +152,140 @@ prompts:
}
}
}
func TestRoleClientCapturesAndReplaysRepairConversation(t *testing.T) {
dir := t.TempDir()
for name, content := range map[string]string{
"config.yml": "version: 3\n",
"prompts.yml": "profiles:\n - id: proposal\n endpoint: https://example.invalid/v1\n model: proposal\n - id: repair\n endpoint: https://example.invalid/v1\n model: repair\n",
} {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml"))}, "")
if err != nil {
t.Fatal(err)
}
var messages [][]structuredoutput.Message
var models []string
outputs := []string{`{"corrections":[]}`, `{"proposal_ref":1,"action":"revise","correction":{"id":1,"original_text":"a","corrected_text":"b","confidence":0.8}}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`}
transport := transportFunc(func(r *http.Request) (*http.Response, error) {
var req struct {
Model string `json:"model"`
Messages []structuredoutput.Message `json:"messages"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatal(err)
}
models = append(models, req.Model)
messages = append(messages, req.Messages)
content, _ := json.Marshal(outputs[len(messages)-1])
body := fmt.Sprintf(`{"model":%q,"choices":[{"message":{"content":%s}}]}`, req.Model, content)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
})
runtime, err := NewRuntime(context.Background(), loaded, &http.Client{Transport: transport})
if err != nil {
t.Fatal(err)
}
settings := config.LLM{OperationTimeout: 10 * time.Second}
proposal, err := runtime.NewRoleClient("proposal", "proposal", settings, CallHooks{})
if err != nil {
t.Fatal(err)
}
repair, err := runtime.NewRoleClient("repair", "repair", settings, CallHooks{})
if err != nil {
t.Fatal(err)
}
vars := map[string]string{"TranscriptDescriptionBlock": "", "GlossaryJSON": "[]", "SectionJSON": "[]"}
initial, err := proposal.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarProposal, Schema: responseschema.CorrectionSetKey, Variables: vars, CaptureExchange: true}, &map[string]any{})
if err != nil || initial.Exchange == nil {
t.Fatalf("capture: %+v %v", initial, err)
}
vars["SectionJSON"] = "changed"
replay := &structuredoutput.Replay{Exchange: initial.Exchange}
for _, finding := range []string{"first", "second"} {
result, err := repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": finding}, Replay: replay}, &map[string]any{})
if err != nil {
t.Fatal(err)
}
if len(result.RepairInstruction) != 1 {
t.Fatalf("instruction: %+v", result)
}
replay.Turns = append(replay.Turns, result.RepairInstruction...)
replay.Turns = append(replay.Turns, structuredoutput.Message{Role: "assistant", Content: result.FinalContent})
}
if !reflect.DeepEqual(models, []string{"proposal", "repair", "repair"}) {
t.Fatalf("models = %v", models)
}
prefix := initial.Exchange.Messages
if len(messages) != 3 || !reflect.DeepEqual(messages[0], prefix) || !reflect.DeepEqual(messages[1][:len(prefix)], prefix) || !reflect.DeepEqual(messages[2][:len(prefix)], prefix) {
t.Fatalf("prefixes: %+v", messages)
}
if len(messages[1]) != len(prefix)+2 || len(messages[2]) != len(prefix)+4 || messages[2][len(prefix)+2].Content != outputs[1] {
t.Fatalf("turns: %+v", messages)
}
_, err = repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "independent"}, Replay: &structuredoutput.Replay{Exchange: initial.Exchange}}, &map[string]any{})
if err != nil || len(messages) != 4 || len(messages[3]) != len(prefix)+2 || !strings.Contains(messages[3][len(prefix)+1].Content, "independent") {
t.Fatalf("independent branch: %v %+v", err, messages)
}
bad := *initial.Exchange
bad.Messages = append([]structuredoutput.Message(nil), bad.Messages...)
bad.Messages[0].Content = "changed"
_, err = repair.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "failure"}, Replay: &structuredoutput.Replay{Exchange: &bad}}, &map[string]any{})
var failure *structuredoutput.Failure
if !errors.As(err, &failure) || failure.Class != structuredoutput.FailureFatal || len(messages) != 4 {
t.Fatalf("prefix failure: %v, calls %d", err, len(messages))
}
}
func TestRepairReplayUsesReplacementAndCountsStructuralRepair(t *testing.T) {
dir := t.TempDir()
files := map[string]string{
"config.yml": "version: 3\n",
"prompts.yml": "profiles:\n - id: fixture\n endpoint: https://example.invalid/v1\n model: fixture\ncollections:\n audita:\n prompts:\n - id: modules.grammar.repair\n messages:\n - role: user\n content_file: repair.txt\n output:\n format: json\n validation_mode: json_schema\n schema_path: correction_repair.v1.json\n",
"repair.txt": "Replace {{.RepairRequestJSON}}",
}
for name, content := range files {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
loaded, err := config.Load(ck.Input{Source: ck.File(filepath.Join(dir, "config.yml"))}, "")
if err != nil {
t.Fatal(err)
}
outputs := []string{`{"corrections":[]}`, `{"bad":true}`, `{"proposal_ref":1,"action":"withdraw","correction":null}`}
var requests [][]structuredoutput.Message
transport := transportFunc(func(r *http.Request) (*http.Response, error) {
var req struct {
Messages []structuredoutput.Message `json:"messages"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatal(err)
}
requests = append(requests, req.Messages)
content, _ := json.Marshal(outputs[len(requests)-1])
body := fmt.Sprintf(`{"model":"fixture","choices":[{"message":{"content":%s}}]}`, content)
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body)), Request: r}, nil
})
runtime, err := NewRuntime(context.Background(), loaded, &http.Client{Transport: transport})
if err != nil {
t.Fatal(err)
}
client, err := runtime.NewRoleClient("repair", "fixture", config.LLM{OperationTimeout: 10 * time.Second, RepairAttempts: 1}, CallHooks{})
if err != nil {
t.Fatal(err)
}
base, err := client.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarProposal, Schema: responseschema.CorrectionSetKey, Variables: map[string]string{"TranscriptDescriptionBlock": "", "GlossaryJSON": "[]", "SectionJSON": "[]"}, CaptureExchange: true}, &map[string]any{})
if err != nil {
t.Fatal(err)
}
result, err := client.CompleteStructured(context.Background(), structuredoutput.Request{PromptID: prompts.PromptIDModuleGrammarRepair, Schema: responseschema.CorrectionRepairKey, Variables: map[string]string{"RepairRequestJSON": "fixture"}, Replay: &structuredoutput.Replay{Exchange: base.Exchange}}, &map[string]any{})
if err != nil {
t.Fatal(err)
}
if len(requests) != 3 || result.Execution.ProviderAttempts != 2 || result.Execution.StructuralRepairs != 1 || result.FinalContent != outputs[2] || len(result.RepairInstruction) != 1 || result.RepairInstruction[0].Content != "Replace fixture" {
t.Fatalf("result=%+v calls=%d", result, len(requests))
}
}

View File

@@ -62,6 +62,7 @@ type Request struct {
Variables map[string]string `json:"variables"`
StageName string `json:"stage_name,omitempty"`
StartIndex int `json:"start_index"`
CaptureExchange bool
LLMClient contracts.StructuredLLMClient
DiagnosticsDir string
DiagnosticsWriter InteractionDiagnosticsWriter
@@ -69,6 +70,7 @@ type Request struct {
// Result contains generated candidate proposals and optional diagnostics paths.
type Result struct {
Exchange *structuredoutput.Exchange `json:"-"`
Corrections []proposals.CorrectionProposal `json:"corrections"`
Enriched []proposals.EnrichedCorrectionProposal `json:"enriched"`
Operations []outcomes.Operation `json:"operations,omitempty"`
@@ -106,8 +108,9 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
artifacts InteractionArtifacts
)
completion, callErr = req.LLMClient.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
PromptID: req.PromptID,
Variables: req.Variables, Schema: responseschema.CorrectionSetKey,
CaptureExchange: req.CaptureExchange,
PromptID: req.PromptID,
Variables: req.Variables, Schema: responseschema.CorrectionSetKey,
Context: structuredoutput.DiagnosticContext{StageIndex: req.StageIndex, StageID: req.StageID, Role: "proposal", ModuleKey: req.ModuleKey, ModuleInstance: req.ModuleInstance, SectionIndex: sectionIndexPtr(req.Section)},
}, &response)
sectionIndex := 0
@@ -216,6 +219,7 @@ func GenerateCandidates(ctx context.Context, req Request) (Result, error) {
return Result{
Corrections: corrections,
Exchange: completion.Exchange,
Enriched: enriched,
Operations: []outcomes.Operation{operation},
Artifacts: artifacts,

View File

@@ -63,6 +63,7 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
Variables: variables,
StageName: stagename.ModuleProposal(req.ProposalRequest.RunSpec.InstanceName, sectionIndexPtr(req.ProposalRequest.Section)),
StartIndex: 0,
CaptureExchange: req.ProposalRequest.RunSpec.CaptureProposalExchange,
LLMClient: req.ProposalRequest.LLMClient,
DiagnosticsDir: req.ProposalRequest.DiagnosticsDir,
ReportDiagnosticError: req.ProposalRequest.ReportDiagnosticError,
@@ -73,6 +74,7 @@ func ExecuteModuleProposal(ctx context.Context, req ModuleProposalRequest) (cont
return contracts.ProposalResult{
Proposals: generated.Corrections,
Exchange: generated.Exchange,
Operations: generated.Operations,
}, nil
}

View File

@@ -18,7 +18,9 @@ import (
)
func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing.T) {
exchange := &structuredoutput.Exchange{PromptID: "captured", FinalContent: "response"}
client := &fakeStructuredClient{
completion: structuredoutput.Response{Exchange: exchange},
responses: []StructuredCorrectionSet{
{Corrections: []StructuredCorrectionProposal{{SegmentReference: 1, OriginalText: "teh", CorrectedText: "the", Confidence: 0.9}}},
},
@@ -39,9 +41,10 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
Section: &section,
},
RunSpec: contracts.ModuleRunSpec{
ModuleKey: "grammar",
InstanceName: "grammar",
ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
CaptureProposalExchange: true,
ModuleKey: "grammar",
InstanceName: "grammar",
ReplacementPolicy: proposals.ReplacementPolicyRequireUnique,
},
LLMClient: client,
},
@@ -53,6 +56,9 @@ func TestExecuteModuleProposalBuildsMessagesFromSectionAndDescription(t *testing
if len(client.calls) != 1 || client.calls[0].PromptID != prompts.PromptIDModuleGrammarProposal || client.calls[0].Context.SectionIndex == nil || *client.calls[0].Context.SectionIndex != 7 {
t.Fatalf("unexpected stage name calls: %+v", client.calls)
}
if !client.calls[0].CaptureExchange || out.Exchange != exchange {
t.Fatalf("exchange was not passed through: %+v", out.Exchange)
}
variables := client.calls[0].Variables
if !strings.Contains(variables["SectionJSON"], `"section_index": 7`) ||
!strings.Contains(variables["SectionJSON"], "teh") ||

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/outcomes"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/promptcontext"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/proposals"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/structuredoutput"
"gitea.maximumdirect.net/eric/audita/v3/internal/framework/validators"
validatormetadata "gitea.maximumdirect.net/eric/audita/v3/internal/validators/metadata"
)
@@ -309,6 +310,7 @@ type sectionProposalResult struct {
sectionPos int
section chunking.Section
corrected []proposals.CorrectionProposal
exchange *structuredoutput.Exchange
operations []outcomes.Operation
started bool
err error
@@ -366,9 +368,10 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
ReportDiagnosticError: input.ReportDiagnosticError,
},
RunSpec: contracts.ModuleRunSpec{
ModuleKey: input.Spec.ModuleKey,
InstanceName: input.Spec.InstanceName,
ReplacementPolicy: input.Policy,
ModuleKey: input.Spec.ModuleKey,
InstanceName: input.Spec.InstanceName,
ReplacementPolicy: input.Policy,
CaptureProposalExchange: input.Spec.CaptureProposalExchange,
},
LLMClient: input.ProposalClient,
})
@@ -376,6 +379,7 @@ func collectSectionProposals(ctx context.Context, input collectSectionProposalsI
sectionPos: sectionPos,
section: section,
corrected: proposalResult.Proposals,
exchange: proposalResult.Exchange,
operations: proposalResult.Operations,
started: true,
err: err,
@@ -504,6 +508,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
SectionIndex: &sectionMetadata.Index,
Validators: validatorsOrdered,
SectionEnriched: enriched,
Exchange: sectionResult.exchange,
ValidationLLMClient: input.ValidationClient,
DiagnosticsDir: input.ValidationDiagnosticsDir,
GlossaryView: input.GlossaryView,
@@ -580,6 +585,7 @@ func runModulePipeline(ctx context.Context, input collectSectionProposalsInput)
}
type validateSectionCandidatesInput struct {
Exchange *structuredoutput.Exchange
// ReportDiagnosticError may be called concurrently; it must not affect processing.
ReportDiagnosticError func(error) `json:"-"`
Spec contracts.ModuleRunSpec

View File

@@ -24,10 +24,12 @@ type DiagnosticContext struct {
// Request names an embedded prompt and response schema. Profile selection and
// credentials are supplied by the role-bound runtime client.
type Request struct {
PromptID string `json:"prompt_id"`
Variables map[string]string `json:"variables"`
Schema responseschema.Key `json:"schema"`
Context DiagnosticContext `json:"context"`
PromptID string `json:"prompt_id"`
Variables map[string]string `json:"variables"`
Schema responseschema.Key `json:"schema"`
Context DiagnosticContext `json:"context"`
CaptureExchange bool `json:"-"`
Replay *Replay `json:"-"`
}
// PromptMetadata is the execution snapshot returned by the runtime when available.
@@ -40,17 +42,33 @@ type PromptMetadata struct {
Model string `json:"model,omitempty"`
}
// Exchange holds a section's original prepared conversation for read-only replay.
type Exchange struct {
PromptID string
Variables map[string]string
Messages []Message
FinalContent string
}
// Replay carries a section exchange and the prior turns of one repair branch.
type Replay struct {
Exchange *Exchange
Turns []Message
}
type Response struct {
Execution outcomes.ExecutionCounts `json:"execution"`
Content json.RawMessage `json:"content,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
Prompt PromptMetadata `json:"prompt"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
Exchange *Exchange `json:"-"`
RepairInstruction []Message `json:"-"`
Execution outcomes.ExecutionCounts `json:"execution"`
Content json.RawMessage `json:"content,omitempty"`
Provider string `json:"provider,omitempty"`
Model string `json:"model,omitempty"`
PromptTokens int `json:"prompt_tokens,omitempty"`
CompletionTokens int `json:"completion_tokens,omitempty"`
TotalTokens int `json:"total_tokens,omitempty"`
Prompt PromptMetadata `json:"prompt"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
}
// PreparedSnapshot is the exact rendered request snapshot. Its messages may
@@ -68,14 +86,15 @@ type Message struct {
// CallRecord brackets one logical operation, including preparation. Content
// is present only when a model returned it; consumers must protect it.
type CallRecord struct {
CallID uint64 `json:"call_id"`
Context DiagnosticContext `json:"context"`
Prompt PromptMetadata `json:"prompt"`
Started time.Time `json:"started"`
Finished time.Time `json:"finished,omitempty"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
Failure FailureKind `json:"failure,omitempty"`
RequestedPromptID string `json:"requested_prompt_id,omitempty"`
CallID uint64 `json:"call_id"`
Context DiagnosticContext `json:"context"`
Prompt PromptMetadata `json:"prompt"`
Started time.Time `json:"started"`
Finished time.Time `json:"finished,omitempty"`
Prepared *PreparedSnapshot `json:"prepared,omitempty"`
FinalContent string `json:"final_content,omitempty"`
Failure FailureKind `json:"failure,omitempty"`
}
type Client interface {