Update the debug workflow to provide raw LLM output

This commit is contained in:
2026-07-07 23:08:14 -05:00
parent ae65b95374
commit 3011dd91ca
10 changed files with 476 additions and 55 deletions

View File

@@ -2,6 +2,7 @@ package pipeline
import (
"context"
"encoding/base64"
"errors"
"reflect"
"strings"
@@ -1289,6 +1290,46 @@ func TestRunRetriesSameModuleInputAfterValidatorRejection(t *testing.T) {
}
}
func TestRunDebugFailedChunkAttemptReferencesScopedLLMOutput(t *testing.T) {
modules := defaultRunnerModules()
modules.chunker.callLLM = true
modules.chunker.llmPromptID = "runner.chunk"
modules.chunker.err = errors.New("malformed structured output")
recorder := newMemoryDebugRecorder()
_, err := New(newRunnerRegistries(t, modules)).Run(context.Background(), RunInput{
Pipeline: resolvedPipeline(),
LLMClient: debugResponseLLMClient{content: []byte(`{"raw":true}`), profileID: "debug-profile"},
Debug: recorder,
})
if err == nil || !strings.Contains(err.Error(), "malformed structured output") {
t.Fatalf("Run() error = %v, want chunk failure", err)
}
attempt := recorder.envelope(t, "chunk/attempt-01.json")
if len(attempt.LLMCalls) != 1 {
t.Fatalf("llm_calls = %#v, want one scoped call", attempt.LLMCalls)
}
call := attempt.LLMCalls[0]
if call.CallID != "call-0001" || call.CanonicalPath != "llm/call-0001.json" || call.ScopedPath != "chunk/attempt-01/llm-call-0001.json" {
t.Fatalf("llm call reference = %#v, want canonical and scoped paths", call)
}
if call.PromptID != "runner.chunk" || call.ProfileID != "debug-profile" || call.Error {
t.Fatalf("llm call metadata = %#v, want prompt/profile and no call error", call)
}
scoped := recorder.envelope(t, call.ScopedPath)
scopedPayload, ok := scoped.Payload.(debugStructuredLLMCall)
if !ok {
t.Fatalf("scoped payload type = %T, want debugStructuredLLMCall", scoped.Payload)
}
wantContent := base64.StdEncoding.EncodeToString([]byte(`{"raw":true}`))
if scopedPayload.Response.Content != wantContent {
t.Fatalf("scoped response content = %q, want %q", scopedPayload.Response.Content, wantContent)
}
_ = recorder.envelope(t, call.CanonicalPath)
}
func TestRunStopsRetryAfterConfiguredAttemptsAndRecordsAttemptCount(t *testing.T) {
modules := defaultRunnerModules()
validator := &runnerChainValidator{name: "chain-extract", approved: []bool{false}, reason: "bad_extract", message: "extract rejected"}
@@ -1915,6 +1956,8 @@ type runnerChunker struct {
err error
failureErr error
failuresBeforeSuccess int
callLLM bool
llmPromptID string
manifestMetadata map[string]any
requests []contracts.ChunkRequest
}
@@ -1937,6 +1980,20 @@ func (chunker *runnerChunker) Chunk(ctx context.Context, req contracts.ChunkRequ
}
return contracts.ChunkResult{}, err
}
if chunker.callLLM && req.LLMClient != nil {
promptID := strings.TrimSpace(chunker.llmPromptID)
if promptID == "" {
promptID = "runner.chunk"
}
var out map[string]any
if _, err := req.LLMClient.CompleteStructured(ctx, contracts.StructuredCompletionRequest{
StageName: chunker.key,
PromptID: promptID,
ProfileID: req.LLMProfile,
}, &out); err != nil {
return contracts.ChunkResult{}, err
}
}
return contracts.ChunkResult{
Chunks: chunker.chunks,
Warnings: chunker.warnings,
@@ -2264,6 +2321,51 @@ func (client fakeLLMClient) CompleteStructured(ctx context.Context, req contract
return contracts.StructuredCompletionResponse{}, nil
}
type debugResponseLLMClient struct {
content []byte
profileID string
err error
}
func (client debugResponseLLMClient) CompleteStructured(ctx context.Context, req contracts.StructuredCompletionRequest, out any) (contracts.StructuredCompletionResponse, error) {
profileID := client.profileID
if profileID == "" {
profileID = req.ProfileID
}
return contracts.StructuredCompletionResponse{
Content: append([]byte(nil), client.content...),
ProfileID: profileID,
}, client.err
}
type memoryDebugRecorder struct {
payloads map[string]any
}
func newMemoryDebugRecorder() *memoryDebugRecorder {
return &memoryDebugRecorder{payloads: map[string]any{}}
}
func (recorder *memoryDebugRecorder) Enabled() bool { return true }
func (recorder *memoryDebugRecorder) WriteJSON(name string, payload any) error {
recorder.payloads[name] = payload
return nil
}
func (recorder *memoryDebugRecorder) envelope(t *testing.T, name string) debugTimedEnvelope {
t.Helper()
payload, ok := recorder.payloads[name]
if !ok {
t.Fatalf("debug artifact %q not written; got %#v", name, recorder.payloads)
}
envelope, ok := payload.(debugTimedEnvelope)
if !ok {
t.Fatalf("debug artifact %q type = %T, want debugTimedEnvelope", name, payload)
}
return envelope
}
type manifestReportingLLMClient struct {
fakeLLMClient
profiles []artifacts.LLMProfileManifest