Files
promptkit/coordinate.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
}