package runnable import ( "context" "fmt" "reflect" ) // Runnable is the base interface for all runnable components. // A Runnable represents a unit of computation that can be invoked. type Runnable[Input, Output any] interface { // Invoke executes the runnable synchronously. Invoke(ctx context.Context, input Input) (Output, error) // Batch executes the runnable on multiple inputs. Batch(ctx context.Context, inputs []Input) ([]Output, []error) // Stream returns a stream of outputs. Stream(ctx context.Context, input Input) <-chan Output // GetSchema returns the input/output schema. GetSchema() *RunnableSchema } // RunnableSchema describes the schema of a runnable. type RunnableSchema struct { InputType string OutputType string Name string Description string } // RunnableFunc wraps a function as a Runnable. type RunnableFunc[Input, Output any] struct { fn func(context.Context, Input) (Output, error) schema *RunnableSchema batchFn func(context.Context, []Input) ([]Output, []error) streamFn func(context.Context, Input) <-chan Output } // NewRunnableFunc creates a new Runnable from a function. func NewRunnableFunc[Input, Output any]( fn func(context.Context, Input) (Output, error), opts ...RunnableOption[Input, Output], ) Runnable[Input, Output] { r := &RunnableFunc[Input, Output]{ fn: fn, schema: &RunnableSchema{ InputType: fmt.Sprintf("%T", *new(Input)), OutputType: fmt.Sprintf("%T", *new(Output)), }, } for _, opt := range opts { opt(r) } return r } // Invoke executes the runnable. func (r *RunnableFunc[Input, Output]) Invoke(ctx context.Context, input Input) (Output, error) { return r.fn(ctx, input) } // Batch executes the runnable on multiple inputs. func (r *RunnableFunc[Input, Output]) Batch(ctx context.Context, inputs []Input) ([]Output, []error) { if r.batchFn != nil { return r.batchFn(ctx, inputs) } // Default: execute sequentially outputs := make([]Output, len(inputs)) errs := make([]error, len(inputs)) for i, input := range inputs { outputs[i], errs[i] = r.Invoke(ctx, input) } return outputs, errs } // Stream returns a stream of outputs. func (r *RunnableFunc[Input, Output]) Stream(ctx context.Context, input Input) <-chan Output { if r.streamFn != nil { return r.streamFn(ctx, input) } // Default: single output ch := make(chan Output, 1) go func() { defer close(ch) output, err := r.Invoke(ctx, input) if err == nil { ch <- output } }() return ch } // GetSchema returns the schema. func (r *RunnableFunc[Input, Output]) GetSchema() *RunnableSchema { return r.schema } // RunnableOption configures a Runnable. type RunnableOption[Input, Output any] func(*RunnableFunc[Input, Output]) // WithName sets the name of the runnable. func WithName[Input, Output any](name string) RunnableOption[Input, Output] { return func(r *RunnableFunc[Input, Output]) { r.schema.Name = name } } // WithDescription sets the description of the runnable. func WithDescription[Input, Output any](desc string) RunnableOption[Input, Output] { return func(r *RunnableFunc[Input, Output]) { r.schema.Description = desc } } // WithBatchFn sets the batch function. func WithBatchFn[Input, Output any]( fn func(context.Context, []Input) ([]Output, []error), ) RunnableOption[Input, Output] { return func(r *RunnableFunc[Input, Output]) { r.batchFn = fn } } // WithStreamFn sets the stream function. func WithStreamFn[Input, Output any]( fn func(context.Context, Input) <-chan Output, ) RunnableOption[Input, Output] { return func(r *RunnableFunc[Input, Output]) { r.streamFn = fn } } // RunnableSeq chains multiple runnables together in sequence. // The output of one runnable is the input to the next. type RunnableSeq struct { runnables []Runnable[any, any] schema *RunnableSchema } // NewRunnableSeq creates a new sequence of runnables. func NewRunnableSeq(runnables ...Runnable[any, any]) (*RunnableSeq, error) { if len(runnables) == 0 { return nil, &RunnableError{Message: "at least one runnable is required"} } // Verify type compatibility (simplified check) for i := 1; i < len(runnables); i++ { prevSchema := runnables[i-1].GetSchema() currSchema := runnables[i].GetSchema() if prevSchema.OutputType != currSchema.InputType { return nil, &RunnableError{ Message: fmt.Sprintf("type mismatch: %s -> %s", prevSchema.OutputType, currSchema.InputType), } } } return &RunnableSeq{ runnables: runnables, schema: &RunnableSchema{ InputType: runnables[0].GetSchema().InputType, OutputType: runnables[len(runnables)-1].GetSchema().OutputType, Name: "sequence", }, }, nil } // Invoke executes all runnables in sequence. func (s *RunnableSeq) Invoke(ctx context.Context, input interface{}) (interface{}, error) { var current interface{} = input var err error for _, r := range s.runnables { current, err = r.Invoke(ctx, current) if err != nil { return nil, err } } return current, nil } // Batch executes the sequence on multiple inputs. func (s *RunnableSeq) Batch(ctx context.Context, inputs []interface{}) ([]interface{}, []error) { outputs := make([]interface{}, len(inputs)) errs := make([]error, len(inputs)) for i, input := range inputs { outputs[i], errs[i] = s.Invoke(ctx, input) } return outputs, errs } // Stream returns a stream of outputs. // On error, sends a StreamError value instead of silently dropping. // Callers can type-assert: if se, ok := val.(StreamError); ok { /* handle err */ } func (s *RunnableSeq) Stream(ctx context.Context, input interface{}) <-chan interface{} { ch := make(chan interface{}, 1) go func() { defer close(ch) output, err := s.Invoke(ctx, input) if err != nil { ch <- StreamError{Err: err} return } ch <- output }() return ch } // GetSchema returns the schema. func (s *RunnableSeq) GetSchema() *RunnableSchema { return s.schema } // RunnableParallel executes multiple runnables in parallel. type RunnableParallel struct { runnables map[string]Runnable[any, any] schema *RunnableSchema } // NewRunnableParallel creates a new parallel runnable. func NewRunnableParallel(runnables map[string]Runnable[any, any]) *RunnableParallel { return &RunnableParallel{ runnables: runnables, schema: &RunnableSchema{ Name: "parallel", InputType: "map", OutputType: "map", }, } } // Invoke executes all runnables in parallel with the same input. func (p *RunnableParallel) Invoke(ctx context.Context, input interface{}) (interface{}, error) { type result struct { name string value interface{} err error } resultCh := make(chan result, len(p.runnables)) for name, r := range p.runnables { go func(n string, rn Runnable[any, any]) { value, err := rn.Invoke(ctx, input) resultCh <- result{name: n, value: value, err: err} }(name, r) } outputs := make(map[string]interface{}) for range p.runnables { res := <-resultCh if res.err != nil { return nil, res.err } outputs[res.name] = res.value } return outputs, nil } // Batch executes the parallel runnable. func (p *RunnableParallel) Batch(ctx context.Context, inputs []interface{}) ([]interface{}, []error) { outputs := make([]interface{}, len(inputs)) errs := make([]error, len(inputs)) for i, input := range inputs { outputs[i], errs[i] = p.Invoke(ctx, input) } return outputs, errs } // Stream returns a stream of outputs. // On error, sends a StreamError value instead of silently dropping. func (p *RunnableParallel) Stream(ctx context.Context, input interface{}) <-chan interface{} { ch := make(chan interface{}, 1) go func() { defer close(ch) output, err := p.Invoke(ctx, input) if err != nil { ch <- StreamError{Err: err} return } ch <- output }() return ch } // GetSchema returns the schema. func (p *RunnableParallel) GetSchema() *RunnableSchema { return p.schema } // RunnableMap transforms the input before passing to the underlying runnable. type RunnableMap struct { inputFn func(context.Context, interface{}) (interface{}, error) outputFn func(context.Context, interface{}) (interface{}, error) base Runnable[any, any] schema *RunnableSchema } // NewRunnableMap creates a new runnable with input/output transformation. func NewRunnableMap( base Runnable[any, any], inputFn func(context.Context, interface{}) (interface{}, error), outputFn func(context.Context, interface{}) (interface{}, error), ) Runnable[any, any] { return &RunnableMap{ base: base, inputFn: inputFn, outputFn: outputFn, schema: &RunnableSchema{ Name: "map", }, } } // Invoke executes the runnable with transformations. func (m *RunnableMap) Invoke(ctx context.Context, input interface{}) (interface{}, error) { if m.inputFn != nil { transformed, err := m.inputFn(ctx, input) if err != nil { return nil, err } input = transformed } output, err := m.base.Invoke(ctx, input) if err != nil { return nil, err } if m.outputFn != nil { transformed, err := m.outputFn(ctx, output) if err != nil { return nil, err } output = transformed } return output, nil } // Batch executes the mapped runnable. func (m *RunnableMap) Batch(ctx context.Context, inputs []interface{}) ([]interface{}, []error) { outputs := make([]interface{}, len(inputs)) errs := make([]error, len(inputs)) for i, input := range inputs { outputs[i], errs[i] = m.Invoke(ctx, input) } return outputs, errs } // Stream returns a stream of outputs. // On error, sends a StreamError value instead of silently dropping. func (m *RunnableMap) Stream(ctx context.Context, input interface{}) <-chan interface{} { ch := make(chan interface{}, 1) go func() { defer close(ch) output, err := m.Invoke(ctx, input) if err != nil { ch <- StreamError{Err: err} return } ch <- output }() return ch } // GetSchema returns the schema. func (m *RunnableMap) GetSchema() *RunnableSchema { return m.schema } // CoerceToRunnable converts various types to a Runnable. // Supported types: Runnable, func(context.Context, T) (U, error), func(T) U func CoerceToRunnable(value interface{}) (Runnable[any, any], error) { switch v := value.(type) { case Runnable[any, any]: return v, nil default: // Check if it's a function valType := reflect.TypeOf(value) if valType == nil { return nil, &RunnableError{ Message: "cannot coerce nil to Runnable", } } if valType.Kind() == reflect.Func { // Try to wrap it as a RunnableFunc return coerceFuncToRunnable(value, valType) } return nil, &RunnableError{ Message: fmt.Sprintf("cannot coerce %T to Runnable", value), } } } // coerceFuncToRunnable coerces a function to a Runnable. func coerceFuncToRunnable(fn interface{}, fnType reflect.Type) (Runnable[any, any], error) { // Check function signature numIn := fnType.NumIn() numOut := fnType.NumOut() // Supported signatures: // 1. func(context.Context, T) (U, error) // 2. func(T) U // 3. func(T) (U, error) // 4. func() U // 5. func() (U, error) // We'll create a wrapper that adapts the function to Runnable[any, any] wrapper := func(ctx context.Context, input interface{}) (interface{}, error) { // Prepare arguments args := make([]reflect.Value, 0, numIn) argIndex := 0 // Check if first argument is context.Context if numIn > 0 && fnType.In(0).AssignableTo(reflect.TypeOf((*context.Context)(nil)).Elem()) { args = append(args, reflect.ValueOf(ctx)) argIndex++ } // Add input argument if needed if argIndex < numIn { inputVal := reflect.ValueOf(input) paramType := fnType.In(argIndex) // Try to convert input to expected type if input != nil && inputVal.Type().AssignableTo(paramType) { args = append(args, inputVal) } else if input != nil && inputVal.Type().ConvertibleTo(paramType) { args = append(args, inputVal.Convert(paramType)) } else { // Use zero value args = append(args, reflect.Zero(paramType)) } argIndex++ } // Fill remaining parameters with zero values for ; argIndex < numIn; argIndex++ { args = append(args, reflect.Zero(fnType.In(argIndex))) } // Call function results := reflect.ValueOf(fn).Call(args) // Process results if numOut == 0 { return nil, nil } else if numOut == 1 { // Single return value result := results[0].Interface() // Check if it's an error if err, ok := result.(error); ok { return nil, err } return result, nil } else if numOut == 2 { // Two return values: result, error result := results[0].Interface() errVal := results[1].Interface() if errVal != nil { if err, ok := errVal.(error); ok { return result, err } return nil, fmt.Errorf("expected error, got %T", errVal) } return result, nil } return nil, &RunnableError{ Message: fmt.Sprintf("unsupported number of return values: %d", numOut), } } return NewRunnableFunc(wrapper), nil } // StreamError wraps an error for stream channels. // Stream implementations send this instead of silently dropping errors. // Callers type-assert: if se, ok := val.(runnable.StreamError); ok { handle(se.Err) }. type StreamError struct { Err error } func (e StreamError) Error() string { if e.Err == nil { return "" } return e.Err.Error() } // RunnableError represents a runnable-related error. type RunnableError struct { Message string Code string } func (e *RunnableError) Error() string { if e.Code != "" { return e.Code + ": " + e.Message } return e.Message } // InvokeCompat provides a compatibility layer for invoke with different input types. func InvokeCompat(ctx context.Context, r Runnable[any, any], input interface{}) (interface{}, error) { return r.Invoke(ctx, input) } // RunnableBuilder provides a fluent interface for building runnables. type RunnableBuilder struct { runnable Runnable[any, any] } // NewRunnableBuilder creates a new runnable builder. func NewRunnableBuilder(runnable Runnable[any, any]) *RunnableBuilder { return &RunnableBuilder{runnable: runnable} } // Then chains another runnable after this one. func (b *RunnableBuilder) Then(next Runnable[any, any]) (*RunnableBuilder, error) { seq, err := NewRunnableSeq(b.runnable, next) if err != nil { return nil, err } return &RunnableBuilder{runnable: seq}, nil } // Map applies input/output transformations. func (b *RunnableBuilder) Map( inputFn func(context.Context, interface{}) (interface{}, error), outputFn func(context.Context, interface{}) (interface{}, error), ) *RunnableBuilder { b.runnable = NewRunnableMap(b.runnable, inputFn, outputFn) return b } // Build returns the final runnable. func (b *RunnableBuilder) Build() Runnable[any, any] { return b.runnable } // Pipe chains runnables in a more ergonomic way. func Pipe(runnables ...Runnable[any, any]) (Runnable[any, any], error) { return NewRunnableSeq(runnables...) } // MapValue transforms the input value. func MapValue( fn func(interface{}) interface{}, ) func(context.Context, interface{}) (interface{}, error) { return func(_ context.Context, v interface{}) (interface{}, error) { return fn(v), nil } }