83 lines
3.8 KiB
Go
83 lines
3.8 KiB
Go
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,
|
|
// including the final permitted response. The initial exchange is not itself a
|
|
// newly delivered result. Each started continuation consumes one turn; retries
|
|
// and structural repairs do not. Callback work runs after admission is released.
|
|
// 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. Context
|
|
// cancellation takes precedence over a callback's returned action or error.
|
|
// Start must be a captured exchange, Next non-nil, and AdditionalTurns
|
|
// nonnegative; invalid values match ErrInvalidRequest. A nil engine returns
|
|
// ErrInvalidConfig. The selected request is copied and its ContinueFrom is
|
|
// replaced by the coordinator's current exchange. The request observer runs
|
|
// before the coordinator observer for each continuation.
|
|
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
|
|
}
|