Files
2026-07-13 12:49:10 +08:00

130 lines
5.2 KiB
Go

/*
* Copyright 2024 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package callbacks provides observability hooks for component execution in Eino.
//
// Callbacks fire at five lifecycle timings around every component invocation:
// - [TimingOnStart] / [TimingOnEnd]: non-streaming input and output.
// - [TimingOnStartWithStreamInput] / [TimingOnEndWithStreamOutput]: streaming
// variants — handlers receive a copy of the stream and MUST close it.
// - [TimingOnError]: component returned a non-nil error (stream-internal
// errors are NOT reported here).
//
// # Attaching Handlers
//
// Global handlers (observe every node in every graph):
//
// callbacks.AppendGlobalHandlers(myHandler) // call once, at startup — NOT thread-safe
//
// Per-invocation handlers (observe one graph run):
//
// runnable.Invoke(ctx, input, compose.WithCallbacks(myHandler))
//
// Target a specific node:
//
// compose.WithCallbacks(myHandler).DesignateNode("nodeName")
//
// Handler inheritance: if the context passed to a graph run already carries
// handlers (e.g. from a parent graph), those handlers are inherited by the
// entire child run automatically.
//
// # Building Handlers
//
// Option 1 — [NewHandlerBuilder]: register raw functions for the timings you
// need. Input/output are untyped; use the component package's ConvCallbackInput
// helper to cast to a concrete type:
//
// handler := callbacks.NewHandlerBuilder().
// OnStartFn(func(ctx context.Context, info *RunInfo, input CallbackInput) context.Context {
// // Handle component start
// return ctx
// }).
// OnEndFn(func(ctx context.Context, info *RunInfo, output CallbackOutput) context.Context {
// // Handle component end
// return ctx
// }).
// OnErrorFn(func(ctx context.Context, info *RunInfo, err error) context.Context {
// // Handle component error
// return ctx
// }).
// OnStartWithStreamInputFn(func(ctx context.Context, info *RunInfo, input *schema.StreamReader[CallbackInput]) context.Context {
// defer input.Close() // MUST close — failure causes pipeline goroutine leak
// return ctx
// }).
// OnEndWithStreamOutputFn(func(ctx context.Context, info *RunInfo, output *schema.StreamReader[CallbackOutput]) context.Context {
// defer output.Close() // MUST close
// return ctx
// }).
// Build()
//
// Option 2 — utils/callbacks.NewHandlerHelper: dispatches by component type, so
// each handler function receives the concrete typed input/output directly:
//
// handler := callbacks.NewHandlerHelper().
// ChatModel(&model.CallbackHandler{
// OnStart: func(ctx context.Context, info *RunInfo, input *model.CallbackInput) context.Context {
// log.Printf("Model started: %s, messages: %d", info.Name, len(input.Messages))
// return ctx
// },
// }).
// Prompt(&prompt.CallbackHandler{
// OnEnd: func(ctx context.Context, info *RunInfo, output *prompt.CallbackOutput) context.Context {
// log.Printf("Prompt completed")
// return ctx
// },
// }).
// Handler()
//
// # Passing State Within a Handler
//
// The ctx returned by one timing is passed to the next timing of the SAME
// handler, enabling OnStart→OnEnd state transfer via context.WithValue:
//
// NewHandlerBuilder().
// OnStartFn(func(ctx context.Context, info *RunInfo, _ CallbackInput) context.Context {
// return context.WithValue(ctx, startTimeKey{}, time.Now())
// }).
// OnEndFn(func(ctx context.Context, info *RunInfo, _ CallbackOutput) context.Context {
// start := ctx.Value(startTimeKey{}).(time.Time)
// log.Printf("duration: %v", time.Since(start))
// return ctx
// }).Build()
//
// Between DIFFERENT handlers there is no guaranteed execution order and no
// context chain. To share state between handlers, store it in a
// concurrency-safe variable in the outermost context instead.
//
// # Common Pitfalls
//
// - Stream copies must be closed: when N handlers register for a streaming
// timing, the stream is copied N+1 times (one per handler + one for
// downstream). If any handler's copy is not closed, the original stream
// cannot be freed and the entire pipeline leaks.
//
// - Do NOT mutate Input/Output: all downstream nodes and handlers share the
// same pointer. Mutations cause data races in concurrent graph execution.
//
// - AppendGlobalHandlers is NOT thread-safe: call only during initialization,
// never concurrently with graph execution.
//
// - Stream errors are invisible to OnError: errors that occur while a
// consumer reads from a StreamReader are not routed through OnError.
//
// - RunInfo may be nil: always nil-check before dereferencing in handlers,
// especially when a component is used standalone outside a graph without
// InitCallbacks being called.
package callbacks