Skip to content

HTTP API ​

Complete Server ​

The example mounts server.Handler in a host-owned HTTP server, protects all routes with a bearer token, disables skill administration, limits chat bodies, and persists threads using JSONL sessions. It binds loopback only.

sh
export OPENAI_API_KEY='your-provider-key'
export AGENT_MODEL='your-model'
export CHAT_API_TOKEN='your-local-access-token'
go run ./example/http-server

SESSION_DIR selects the storage directory; it defaults to ./sessions. OPENAI_BASE_URL selects a compatible provider endpoint if needed. Keep tokens out of shell history and source control in real deployments.

go
package main

import (
	"context"
	"crypto/subtle"
	"errors"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"strings"
	"syscall"
	"time"

	"github.com/covoyage/covonaut/agentcore"
	"github.com/covoyage/covonaut/provider/chatcompat"
	"github.com/covoyage/covonaut/server"
	"github.com/covoyage/covonaut/session"
)

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	apiKey, model, token := os.Getenv("OPENAI_API_KEY"), os.Getenv("AGENT_MODEL"), os.Getenv("CHAT_API_TOKEN")
	if apiKey == "" || model == "" || token == "" {
		return errors.New("set OPENAI_API_KEY, AGENT_MODEL, and CHAT_API_TOKEN")
	}
	directory := os.Getenv("SESSION_DIR")
	if directory == "" {
		directory = "./sessions"
	}
	sessions, err := session.NewFileStore(directory)
	if err != nil {
		return err
	}
	cfg := agentcore.NewConfig(
		agentcore.WithModel(model),
		agentcore.WithProvider(chatcompat.New(chatcompat.Config{
			APIKey: apiKey, BaseURL: os.Getenv("OPENAI_BASE_URL"),
		})),
		agentcore.WithStore(session.NewAgentStore(sessions, ".")),
		agentcore.WithStreaming(true),
		agentcore.WithMaxTurns(10),
	)
	cfg.DisableSkillRegistryAPI = true
	cfg.DisableSkillReloadAPI = true
	application := server.New(cfg)
	defer application.Close()
	application.SetMaxRequestBodyBytes(1 << 20)
	handler := application.Handler()
	protected := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		provided := strings.TrimPrefix(request.Header.Get("Authorization"), "Bearer ")
		if !strings.HasPrefix(request.Header.Get("Authorization"), "Bearer ") ||
			subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
			http.Error(writer, "unauthorized", http.StatusUnauthorized)
			return
		}
		handler.ServeHTTP(writer, request)
	})
	httpServer := &http.Server{
		Addr: "127.0.0.1:8080", Handler: protected,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       30 * time.Second,
		IdleTimeout:       2 * time.Minute,
	}
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	finished := make(chan error, 1)
	go func() { finished <- httpServer.ListenAndServe() }()
	fmt.Println("Chat API: http://127.0.0.1:8080")
	select {
	case err := <-finished:
		if errors.Is(err, http.ErrServerClosed) {
			return nil
		}
		return err
	case <-ctx.Done():
		shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		return httpServer.Shutdown(shutdownCtx)
	}
}

Chat Requests ​

sh
curl http://127.0.0.1:8080/api/chat \
  -H "Authorization: Bearer $CHAT_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"message":"Hello","stream":false}'

A session-backed store creates a thread when none is supplied. Keep the returned thread_id and send it with subsequent requests to continue that conversation. For streaming, set stream to true and use a client that parses SSE; curl -N is useful for local inspection.

model, response_format, thinking, and skills are per-request settings. The effective precedence is server defaults, then saved thread configuration, then supplied request overrides. An omitted setting is not necessarily a request to clear a saved setting.

Threads, Branches, and State ​

Sessions and HTTP lists thread endpoints. Branch a historical message using its returned entry_id, not an assumed array position. Thread config and thinking endpoints can persist or clear their overrides using their documented request envelope.

Generic /api/states endpoints are separate from the session-backed thread view. The /agui/{path} mount uses AG-UI request semantics, not the ChatRequest JSON above.

Security and Lifetime ​

The framework server is not a complete authentication service. The example's shared token is suitable for a local single-user demo, not tenant isolation. Production hosts must authorize every thread/state operation, bind ownership to an authenticated user, restrict allowed model overrides, and enforce quotas.

CORS is a browser policy, not authentication. Configure explicit origins when needed; do not expose unrestricted state endpoints behind a permissive proxy. SetMaxRequestBodyBytes limits the server's chat/admin decoding paths, but do not assume it also limits a separately mounted AG-UI handler. Apply host-level body limits and rate limits to every route.

When mounting Handler, the host owns HTTP shutdown; close the application after draining requests. Long-lived SSE needs appropriate proxy buffering and timeout configuration. Bound model/tool execution separately: HTTP read timeouts alone do not bound reasoning duration.

Released under the MIT License.