package promptkit import ( "context" "fmt" "go.maximumdirect.net/promptkit/internal/domain" "go.maximumdirect.net/promptkit/internal/usecase" ) // ConversationTurn is delivered before each requested continuation. Result is // nil on the initial callback; RemainingTurns counts additional runs allowed. type ConversationTurn struct { Exchange *Exchange Result *RunResult RemainingTurns int } // CoordinateRequest selects a starting exchange, turn budget and callback. // Observer selects continuation runs only, independently of callback calls. type CoordinateRequest struct { Start *Exchange AdditionalTurns int Next func(context.Context, ConversationTurn) (*RunRequest, error) Observer ExecutionObserver } // CoordinationOutcome distinguishes caller-directed stopping from exhaustion. type CoordinationOutcome string const ( // CoordinationStopped means the callback chose not to continue. CoordinationStopped CoordinationOutcome = "stopped" // CoordinationExhausted means another run was requested without budget. CoordinationExhausted CoordinationOutcome = "exhausted" ) // CoordinationResult contains the normal outcome or a response withheld by // cancellation before delivery to the callback. type CoordinationResult struct { Outcome CoordinationOutcome `json:"outcome,omitempty"` Undelivered *RunResult `json:"undelivered,omitempty"` } // Coordinate advances one branch from Start using ordinary runs. Next is called // initially even for a zero budget and receives each completed result once. // A nil request stops; requesting a run without remaining budget exhausts it. // Callback and execution errors retain their identities. Cancellation observed // before delivery returns the single completed response as Undelivered; // cancellation after callback invocation does not replay that response. func (e *Engine) Coordinate(ctx context.Context, req CoordinateRequest) (CoordinationResult, error) { if e == nil || e.internal == nil { return CoordinationResult{}, fmt.Errorf("%w: engine is nil", ErrInvalidConfig) } if req.Start == nil || req.Start.internal == nil || req.Next == nil || req.AdditionalTurns < 0 { return CoordinationResult{}, fmt.Errorf("%w: invalid coordination request", ErrInvalidRequest) } outcome, err := usecase.Coordinate(ctx, req.Start.internal, req.AdditionalTurns, func(ctx context.Context, turn usecase.ConversationTurn[RunResult]) (*RunRequest, error) { return req.Next(ctx, ConversationTurn{Exchange: &Exchange{internal: turn.Exchange}, Result: turn.Result, RemainingTurns: turn.RemainingTurns}) }, func(ctx context.Context, current *domain.Exchange, selected *RunRequest) (*RunResult, *domain.Exchange, error) { request := *selected request.ContinueFrom = &Exchange{internal: current} request.Observer = ComposeObservers(request.Observer, req.Observer) result, runErr := e.Run(ctx, request) if runErr != nil { return nil, nil, runErr } return result, result.Exchange.internal, nil }) return CoordinationResult{Outcome: CoordinationOutcome(outcome.Outcome), Undelivered: outcome.Undelivered}, err }