Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 51 additions & 15 deletions go/sigil/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ type Config struct {
GenerationExport GenerationExportConfig
API APIConfig
EmbeddingCapture EmbeddingCaptureConfig
// ContentCapture controls the default content capture mode for all
// generations and tool executions. Per-recording overrides take precedence.
ContentCapture ContentCaptureMode
// ContentCaptureResolver, when set, is called before each generation,
// tool execution, and rating submission to dynamically resolve the content
// capture mode. It receives the request context and the recording's
// metadata (nil when the recording type has no metadata, e.g. tool
// executions).
//
// Resolution precedence (highest → lowest):
// 1. Per-recording ContentCapture field (explicit override)
// 2. ContentCaptureResolver return value
// 3. Config.ContentCapture (static default)
//
// Returning ContentCaptureModeDefault defers to Config.ContentCapture.
// Panics are recovered and treated as ContentCaptureModeMetadataOnly
// (fail-closed).
ContentCaptureResolver func(ctx context.Context, metadata map[string]any) ContentCaptureMode
// Tracer is optional and mainly used for tests. If nil, the client uses the global OpenTelemetry tracer.
Tracer trace.Tracer
// Meter is optional and mainly used for tests. If nil, the client uses the global OpenTelemetry meter.
Expand Down Expand Up @@ -226,11 +244,12 @@ type Client struct {
//
// All methods are safe to call on a nil or no-op recorder.
type GenerationRecorder struct {
client *Client
ctx context.Context
span trace.Span
seed GenerationStart
startedAt time.Time
client *Client
ctx context.Context
span trace.Span
seed GenerationStart
startedAt time.Time
contentCaptureMode ContentCaptureMode

mu sync.Mutex
ended bool
Expand Down Expand Up @@ -455,12 +474,19 @@ func (c *Client) startGeneration(ctx context.Context, start GenerationStart, def
ThinkingEnabled: cloneBoolPtr(seed.ThinkingEnabled),
})...)

// Resolve content capture mode: per-recording > resolver > client default.
resolverMode := callContentCaptureResolver(c.config.ContentCaptureResolver, ctx, seed.Metadata)
clientMode := resolveClientContentCaptureMode(resolveContentCaptureMode(resolverMode, c.config.ContentCapture))
ccMode := resolveContentCaptureMode(seed.ContentCapture, clientMode)
callCtx = withContentCaptureMode(callCtx, ccMode)

return callCtx, &GenerationRecorder{
client: c,
ctx: callCtx,
span: span,
seed: seed,
startedAt: startedAt,
client: c,
ctx: callCtx,
span: span,
seed: seed,
startedAt: startedAt,
contentCaptureMode: ccMode,
}
}

Expand Down Expand Up @@ -575,13 +601,19 @@ func (c *Client) StartToolExecution(ctx context.Context, start ToolExecutionStar
attrs := toolSpanAttributes(seed)
span.SetAttributes(attrs...)

// Resolve content capture: per-tool > context (parent generation) > resolver > client default.
resolverMode := callContentCaptureResolver(c.config.ContentCaptureResolver, ctx, nil)
effectiveClientDefault := resolveContentCaptureMode(resolverMode, c.config.ContentCapture)
ctxMode, ctxSet := contentCaptureModeFromContext(ctx)
includeContent := shouldIncludeToolContent(seed.ContentCapture, ctxMode, ctxSet, effectiveClientDefault, seed.IncludeContent)

return callCtx, &ToolExecutionRecorder{
client: c,
ctx: callCtx,
span: span,
seed: seed,
startedAt: startedAt,
includeContent: seed.IncludeContent,
includeContent: includeContent,
}
}

Expand Down Expand Up @@ -661,14 +693,19 @@ func (r *GenerationRecorder) End() {
normalized := r.normalizeGeneration(generation, completedAt, callErr)
applyTraceContextFromSpan(r.span, &normalized)

stampContentCaptureMetadata(&normalized, r.contentCaptureMode)
if r.contentCaptureMode == ContentCaptureModeMetadataOnly {
stripContent(&normalized, classifyErrorCategory(callErr, false))
}

r.span.SetName(generationSpanName(normalized))
r.span.SetAttributes(generationSpanAttributes(normalized)...)

r.mu.Lock()
r.lastGeneration = cloneGeneration(normalized)
r.mu.Unlock()

enqueueErr := r.client.persistGeneration(r.ctx, normalized)
enqueueErr := r.client.persistGeneration(normalized)

// Record errors on span.
if callErr != nil {
Expand Down Expand Up @@ -1078,11 +1115,10 @@ func combineAllErrors(errs ...error) error {
return errors.Join(filtered...)
}

func (c *Client) persistGeneration(_ context.Context, generation Generation) error {
if err := ValidateGeneration(generation); err != nil {
func (c *Client) persistGeneration(generation Generation) error {
if err := validateGeneration(generation); err != nil {
return fmt.Errorf("%w: %v", errGenerationValidation, err)
}

if err := c.enqueueGeneration(generation); err != nil {
return fmt.Errorf("%w: %w", errGenerationEnqueue, err)
}
Expand Down
84 changes: 41 additions & 43 deletions go/sigil/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1086,59 +1086,57 @@ func TestStartToolExecutionSetsExecuteToolAttributes(t *testing.T) {
}

func TestToolExecutionRecorderContentCapture(t *testing.T) {
// Backward compat: Config{} with IncludeContent controls tool content.
client, recorder, _ := newTestClient(t, Config{})

_, withContent := client.StartToolExecution(context.Background(), ToolExecutionStart{
ToolName: "weather",
IncludeContent: true,
})
withContent.SetResult(ToolExecutionEnd{
Arguments: map[string]any{"city": "Paris"},
Result: map[string]any{"temp_c": 18},
})
withContent.End()
if err := withContent.Err(); err != nil {
t.Fatalf("end tool execution with content: %v", err)
}

_, withoutContent := client.StartToolExecution(context.Background(), ToolExecutionStart{
ToolName: "weather",
})
withoutContent.SetResult(ToolExecutionEnd{
Arguments: map[string]any{"city": "Paris"},
Result: map[string]any{"temp_c": 18},
})
withoutContent.End()
if err := withoutContent.Err(); err != nil {
t.Fatalf("end tool execution without content: %v", err)
}

toolSpans := make([]sdktrace.ReadOnlySpan, 0, 2)
for _, span := range recorder.Ended() {
if isToolSpan(span) {
toolSpans = append(toolSpans, span)
execTool := func(t *testing.T, start ToolExecutionStart) sdktrace.ReadOnlySpan {
t.Helper()
start.ToolName = "weather"
_, rec := client.StartToolExecution(context.Background(), start)
rec.SetResult(ToolExecutionEnd{
Arguments: map[string]any{"city": "Paris"},
Result: map[string]any{"temp_c": 18},
})
rec.End()
if err := rec.Err(); err != nil {
t.Fatalf("tool execution error: %v", err)
}
}
if len(toolSpans) != 2 {
t.Fatalf("expected 2 tool spans, got %d", len(toolSpans))
spans := recorder.Ended()
for i := len(spans) - 1; i >= 0; i-- {
if isToolSpan(spans[i]) {
return spans[i]
}
}
t.Fatal("tool span not found")
return nil
}

var sawWithContent, sawWithoutContent bool
for _, span := range toolSpans {
hasContent := func(span sdktrace.ReadOnlySpan) bool {
attrs := spanAttributeMap(span)
_, hasArgs := attrs[spanAttrToolCallArguments]
_, hasResult := attrs[spanAttrToolCallResult]
if hasArgs && hasResult {
sawWithContent = true
return hasArgs
}

t.Run("Config{} + bare ToolExecutionStart — no content", func(t *testing.T) {
span := execTool(t, ToolExecutionStart{})
if hasContent(span) {
t.Fatal("expected no tool content with default config and no IncludeContent")
}
if !hasArgs && !hasResult {
sawWithoutContent = true
})

t.Run("Config{} + IncludeContent:true — content included", func(t *testing.T) {
span := execTool(t, ToolExecutionStart{IncludeContent: true})
if !hasContent(span) {
t.Fatal("expected tool content with IncludeContent: true")
}
}
})

if !sawWithContent || !sawWithoutContent {
t.Fatalf("expected both content and non-content tool spans")
}
t.Run("Config{} + IncludeContent:false — no content", func(t *testing.T) {
span := execTool(t, ToolExecutionStart{IncludeContent: false})
if hasContent(span) {
t.Fatal("expected no tool content with IncludeContent: false")
}
})
}

func TestToolExecutionRecorderErrorSetsStatusAndType(t *testing.T) {
Expand Down
Loading
Loading