Vectrum
Sign In
109 lines · 2.8 KB · Go
// Command harness is the CLI entry point: flag parsing, wiring, and the REPL.
package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	"harness/internal/agent"
	"harness/internal/llm"
	"harness/internal/metrics"
	"harness/internal/tools"
	"harness/internal/ui"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "harness:", err)
		os.Exit(1)
	}
}

func run() error {
	workdir := flag.String("C", ".", "working root the agent operates in")
	model := flag.String("model", llm.DefaultModel, "model id")
	effort := flag.String("effort", "", "output_config.effort: low|medium|high|xhigh|max (default: the API's, high)")
	maxTurns := flag.Int("max-turns", agent.DefaultMaxTurns, "turn cap per prompt")
	logPath := flag.String("metrics", metrics.DefaultPath, "JSONL event log path, relative to the working root")
	prompt := flag.String("p", "", "run one prompt and exit, instead of starting the REPL")
	verbose := flag.Bool("v", false, "print full tool output")
	flag.Parse()

	root, err := filepath.Abs(*workdir)
	if err != nil {
		return err
	}

	session := time.Now().UTC().Format("20060102T150405Z")
	log, err := metrics.Open(filepath.Join(root, *logPath), session)
	if err != nil {
		return err
	}
	defer log.Close()
	log.Log(metrics.Event{Type: "session_start", Extra: map[string]any{"root": root, "model": *model}})

	client := llm.New(llm.WithModel(*model))
	if !client.HasCredentials() {
		return fmt.Errorf("no credentials: export ANTHROPIC_API_KEY, or run `ant auth login`")
	}

	state := agent.NewState()
	reg := tools.NewRegistry(log).Register(
		tools.NewRead(root, state),
	)

	render := &ui.Renderer{W: os.Stdout, Verbose: *verbose}
	a := agent.New(client, reg, state, log, agent.Options{MaxTurns: *maxTurns, Effort: *effort})
	a.Observe = render.Handle

	ctx := context.Background()
	if *prompt != "" {
		return once(ctx, a, render, *prompt)
	}

	fmt.Printf("harness · %s · %s\ntools: %s\ntype a prompt, or ctrl-d to exit\n\n",
		*model, root, strings.Join(toolNames(reg), ", "))

	in := bufio.NewScanner(os.Stdin)
	in.Buffer(make([]byte, 0, 64*1024), 1024*1024)
	for {
		fmt.Print("> ")
		if !in.Scan() {
			fmt.Println()
			return in.Err()
		}
		line := strings.TrimSpace(in.Text())
		if line == "" {
			continue
		}
		if line == "/exit" || line == "/quit" {
			return nil
		}
		if err := once(ctx, a, render, line); err != nil {
			render.Error(err)
		}
		fmt.Println()
	}
}

// once runs a single prompt and prints the session totals afterwards.
func once(ctx context.Context, a *agent.Agent, render *ui.Renderer, prompt string) error {
	_, err := a.Run(ctx, prompt)
	usage, turns := a.State().Usage()
	render.Usage(usage, turns)
	return err
}

func toolNames(reg *tools.Registry) []string {
	var names []string
	for _, t := range reg.List() {
		names = append(names, t.Name())
	}
	return names
}