Keep observer composition storage proportional to selected callbacks

This commit is contained in:
2026-11-14 14:29:39 +00:00
parent 64cf84c9b5
commit 892cb74e6e
3 changed files with 47 additions and 5 deletions

View File

@@ -140,7 +140,13 @@ statically. Do not add public allocation guarantees or exact-allocation tests.
## 3. Size Observer Storage For Selected Callbacks ## 3. Size Observer Storage For Selected Callbacks
**Priority:** P3. **Status:** For consideration. **Plan stage:** 9. **Priority:** P3. **Status:** Complete. **Plan stage:** 9.
**Completion:** Observer composition counts selected callbacks before allocating,
sizes retained storage to that count, and handles zero/single selections without
a fan-out closure. Sparse-selection ownership and duplicate-delivery tests pass.
The complete offline race suite, maintained examples, vet, and diff checks passed
with Go 1.27.3 after all remediations.
**Location:** [`Compose`](../../internal/observation/compose.go). **Location:** [`Compose`](../../internal/observation/compose.go).

View File

@@ -2,15 +2,26 @@ package observation
// Compose copies and synchronously invokes non-nil observers in order. // Compose copies and synchronously invokes non-nil observers in order.
func Compose[T any, F ~func(T)](observers ...F) F { func Compose[T any, F ~func(T)](observers ...F) F {
selected := make([]F, 0, len(observers)) count := 0
var single F
for _, observer := range observers {
if observer != nil {
count++
single = observer
}
}
if count == 0 {
return nil
}
if count == 1 {
return single
}
selected := make([]F, 0, count)
for _, observer := range observers { for _, observer := range observers {
if observer != nil { if observer != nil {
selected = append(selected, observer) selected = append(selected, observer)
} }
} }
if len(selected) == 0 {
return nil
}
return func(event T) { return func(event T) {
for _, observer := range selected { for _, observer := range selected {
observer(event) observer(event)

View File

@@ -31,6 +31,31 @@ func TestComposeObserversCopiesAndOrders(t *testing.T) {
promptkit.ComposeObservers(func(promptkit.ExecutionEvent) { panic("panic") }, second)(promptkit.ExecutionEvent{}) promptkit.ComposeObservers(func(promptkit.ExecutionEvent) { panic("panic") }, second)(promptkit.ExecutionEvent{})
} }
func TestComposeObserversSparseSelection(t *testing.T) {
for _, selected := range []int{0, 1, 2} {
calls := 0
observers := make([]promptkit.ExecutionObserver, 1024)
callback := func(promptkit.ExecutionEvent) { calls++ }
for index := 0; index < selected; index++ {
observers[len(observers)-1-index] = callback
}
composed := promptkit.ComposeObservers(observers...)
for index := range observers {
observers[index] = func(promptkit.ExecutionEvent) { calls += 100 }
}
if selected == 0 {
if composed != nil {
t.Fatal("all-nil composition must be nil")
}
continue
}
composed(promptkit.ExecutionEvent{})
if calls != selected {
t.Fatalf("selected %d callbacks, got %d calls", selected, calls)
}
}
}
func TestCoordinateObserverCompositionAndNestedSelection(t *testing.T) { func TestCoordinateObserverCompositionAndNestedSelection(t *testing.T) {
engine := newSimpleEngine(t, "question", promptkit.WithLLMClient(&fakeLLMClient{response: &promptkit.GenerateResponse{Content: "answer"}})) engine := newSimpleEngine(t, "question", promptkit.WithLLMClient(&fakeLLMClient{response: &promptkit.GenerateResponse{Content: "answer"}}))
ctx := context.Background() ctx := context.Background()