diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 39afaf1..e6d875d 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -74,7 +74,10 @@ func Run(ctx context.Context, options Options) (Summary, error) { // Pick the action and extractor sources once from the driver's // capabilities so the step loop runs one uniform path with no per-step // driver type assertion. - actionSource, extractorSource := pickSources(options) + actionSource, extractorSource, err := pickSources(options) + if err != nil { + return Summary{}, err + } summary := Summary{StartTime: time.Now()} deadline := summary.StartTime.Add(options.Duration) @@ -217,6 +220,7 @@ func Run(ctx context.Context, options Options) (Summary, error) { var traceAction *trace.Action if nextErr == nil { traceAction = traceActionFor(nextAction, tree) + stampActionSource(traceAction, actionSource) } else if !errors.Is(nextErr, verifier.ErrNoAction) { return summary, fmt.Errorf("step %d next action: %w", stepIndex, nextErr) } diff --git a/internal/runner/source.go b/internal/runner/source.go index f2a7367..5af25ac 100644 --- a/internal/runner/source.go +++ b/internal/runner/source.go @@ -4,8 +4,10 @@ import ( "context" "encoding/json" "fmt" + "log/slog" "github.com/priyanshujain/sanderling/internal/driver" + "github.com/priyanshujain/sanderling/internal/openrouter" "github.com/priyanshujain/sanderling/internal/verifier" ) @@ -61,12 +63,33 @@ func (s webSource) ExtractorOverrides(ctx context.Context) (map[int]json.RawMess } // pickSources selects the runtime's action and extractor sources ONCE at setup -// from the driver's capabilities, so the step loop never type-asserts. -func pickSources(options Options) (ActionSource, ExtractorSource) { +// from the driver's capabilities and the spec, so the step loop never +// type-asserts. When the spec selected the LLM action backend (actions = +// llm({...})) it constructs the OpenRouter client and returns an llmSource for +// selection while extractor overrides still come from the goja path. +func pickSources(options Options) (ActionSource, ExtractorSource, error) { if web, ok := options.Driver.(driver.WebDriver); ok { source := webSource{web: web} - return source, source + return source, source, nil + } + if config, ok := options.Verifier.LLMConfig(); ok { + client, err := openrouter.New() + if err != nil { + return nil, nil, fmt.Errorf("llm action backend: %w", err) + } + logger := options.Logger + if logger == nil { + logger = slog.Default() + } + action := &llmSource{ + verifier: options.Verifier, + client: client, + model: config.Model, + logger: logger, + history: newActionHistory(llmHistorySize), + } + return action, gojaSource{verifier: options.Verifier}, nil } source := gojaSource{verifier: options.Verifier} - return source, source + return source, source, nil }