agentfleet

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 19 Imported by: 0

README

agentfleet

Run your AI agent fleet from a single terminal dashboard

agentfleet is a Go library for running multiple interactive CLI sessions (Claude Code, Codex, or any command) in parallel with a unified Bubbletea TUI dashboard. Each agent runs independently in a PTY with optional step injection, session recording, and remote attach capabilities via Unix sockets.

Install

Use as a library:

import agentfleet "github.com/hoaitan/agentfleet"

Or try an example directly from source:

git clone https://github.com/hoaitan/agentfleet
cd agentfleet/examples/file-manager
go run . --source tasks.md

Library Quick Start

package main

import (
    "context"
    "os/signal"
    "syscall"

    agentfleet "github.com/hoaitan/agentfleet"
    "github.com/hoaitan/agentfleet/tui"
)

func main() {
    cfg := agentfleet.DefaultConfig()
    cfg.Agent = agentfleet.AgentConfigFromTerminal()

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    fleet := agentfleet.NewFleet(cfg.Fleet)

    // Implement Manager to drive tasks into the Fleet:
    // mgr := &MyManager{...}
    // go mgr.Run(ctx, fleet)

    tui.Run(ctx, fleet, cfg.TUI, nil)
}

Examples

Example Purpose
http-manager Load tasks from an HTTP endpoint and run agents
file-manager Load tasks from JSON, YAML, or Markdown and run agents
generate-manager Generate tasks via Claude API and run agents

Fleet Dashboard

┌─────────────────────────────────────────────────────────────────┐
│ agentfleet — 4 agents running                                   │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────────┐  ┌──────────────────┐                      │
│ │ task-1: Running  │  │ task-2: Running  │                      │
│ │ claude           │  │ claude           │                      │
│ │                  │  │                  │                      │
│ │ $ What is today  │  │ $ Tell me a joke │                      │
│ │ > December 19... │  │ > Why did the... │                      │
│ └──────────────────┘  └──────────────────┘                      │
│ ┌──────────────────┐  ┌──────────────────┐                      │
│ │ task-3: Done     │  │ task-4: Failed   │                      │
│ │ codex            │  │ vim              │                      │
│ │                  │  │                  │                      │
│ └──────────────────┘  └──────────────────┘                      │
├─────────────────────────────────────────────────────────────────┤
│ (j/k) scroll | (↵) attach | (q) quit                            │
└─────────────────────────────────────────────────────────────────┘
Key Bindings
Key Action
j / k Navigate between agents (scroll down/up)
Enter Attach to selected agent's terminal
q Quit (stops all running agents)

Extending

Custom Manager

Implement Manager to control how tasks are loaded and when they run:

package mymgr

import (
    "context"
    agentfleet "github.com/hoaitan/agentfleet"
)

type GRPCManager struct{ client pb.TaskClient }

func (m *GRPCManager) Run(ctx context.Context, fleet *agentfleet.Fleet) error {
    stream, _ := m.client.TaskStream(ctx)
    for {
        task, err := stream.Recv()
        if err != nil { return err }
        ag := agentfleet.NewPtyAgent(agentfleet.CommandFields(task))
        r  := agentfleet.NewRunner(task, ag, agentfleet.DefaultConfig().Fleet)
        fleet.Add(ctx, r)
        r.Start()
        go func(r *agentfleet.Runner) {
            <-r.Done()
            stream.Send(&pb.Result{Id: r.Task().ID(), Status: r.Status().String()})
        }(r)
    }
}
Custom Hook
package myhooks

import "github.com/hoaitan/agentfleet/hook"

type MyHook struct{}

func (h *MyHook) Process(data []byte, dir hook.Dir) ([]byte, error) {
    if dir == hook.DirOut {
        // Transform agent output: redact secrets, annotate, etc.
    }
    return data, nil
}
Custom Task
package mytasks

import agentfleet "github.com/hoaitan/agentfleet"

type MyTask struct {
    agentfleet.BasicTask
    CustomField string
}
Custom Source
package mysource

import (
    agentfleet "github.com/hoaitan/agentfleet"
    "github.com/hoaitan/agentfleet/source"
)

type DatabaseSource struct{ URL string }

func (s *DatabaseSource) Load() ([]agentfleet.Task, error) {
    // Query database, return []agentfleet.Task
}

Architecture

Package Responsibility
github.com/hoaitan/agentfleet Core: Task, Fleet, Runner, Manager, Agent, Config
agentfleet/tui Bubbletea TUI dashboard — tui.Run(ctx, fleet, cfg, onAttach)
agentfleet/source Task loaders: FileSource, MarkdownSource, HTTPSource, GenerateSource, StepTask
agentfleet/hook Byte processing: Hook, Chain, FileLogger, Logger
examples/http-manager Example: load tasks from HTTP endpoint; includes taskserver/ sub-directory
examples/file-manager Example: load tasks from JSON/YAML/Markdown
examples/generate-manager Example: generate tasks with Claude API

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CommandFields

func CommandFields(t Task) []string

CommandFields splits Task.Command() into argv for NewPtyAgent.

Types

type Agent

type Agent interface {
	Start(rows, cols int) error
	Write(p []byte) (int, error)
	Read(p []byte) (int, error)
	Resize(rows, cols int) error
	Stop() error
	Done() <-chan struct{}
}

Agent is any interactive CLI process running in a PTY.

type AgentConfig

type AgentConfig struct {
	PTYRows int      // default: 24
	PTYCols int      // default: 220
	Env     []string // extra env vars (KEY=VALUE); appended to os.Environ() for child process only
}

AgentConfig controls PTY dimensions and environment.

func AgentConfigFromTerminal

func AgentConfigFromTerminal() AgentConfig

AgentConfigFromTerminal reads actual terminal dimensions. Falls back to DefaultConfig().Agent when stdout is not a TTY.

type BasicTask

type BasicTask struct {
	TaskID   string `json:"id"      yaml:"id"`
	TaskName string `json:"name"    yaml:"name"`
	Cmd      string `json:"command" yaml:"command"`
}

BasicTask is the default Task implementation.

func (*BasicTask) Command

func (t *BasicTask) Command() string

func (*BasicTask) ID

func (t *BasicTask) ID() string

func (*BasicTask) Name

func (t *BasicTask) Name() string

type Config

type Config struct {
	Fleet FleetConfig
	TUI   TUIConfig
	Agent AgentConfig
}

Config holds all configuration for a fleet run.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig returns sensible production defaults.

type Fleet

type Fleet struct {
	// contains filtered or unexported fields
}

Fleet is a thread-safe, dynamic registry of Runners. Managers add Runners via Add(); the TUI reads via Runners(). Fleet enforces MaxConcurrent: Add() blocks until a slot is available.

func NewFleet

func NewFleet(cfg FleetConfig) *Fleet

func (*Fleet) Add

func (f *Fleet) Add(ctx context.Context, r *Runner) error

Add registers a Runner with the Fleet and blocks until a concurrency slot is available. Returns ctx.Err() if the context is cancelled while waiting. The Runner must already be Start()-ed before calling Add().

func (*Fleet) Remove added in v0.6.24

func (f *Fleet) Remove(taskID string)

Remove immediately drops the runner with the given taskID from the fleet list. The runner's Done() goroutine still handles the semaphore release when the PTY exits — callers should stop the runner separately before or after calling Remove.

func (*Fleet) Runners

func (f *Fleet) Runners() []*Runner

Runners returns a snapshot of all registered Runners.

func (*Fleet) Wait

func (f *Fleet) Wait(ctx context.Context) error

Wait blocks until all Runners have completed or ctx is cancelled.

type FleetConfig

type FleetConfig struct {
	MaxConcurrent int    // max tasks running in parallel — default: 9
	VTERows       int    // virtual terminal height per runner — default: 200
	SocketDir     string // Unix socket dir; empty = no socket server — default: /tmp
	LogDir        string // session log dir; empty = no log file    — default: /tmp
}

FleetConfig controls task scheduling and I/O paths.

type LogBuffer added in v0.6.0

type LogBuffer struct {
	// contains filtered or unexported fields
}

LogBuffer is a thread-safe line-oriented ring buffer that implements io.Writer. Pass it to slog.NewTextHandler to capture log output; read Lines() in the TUI.

func NewLogBuffer added in v0.6.0

func NewLogBuffer(maxLines int) *LogBuffer

NewLogBuffer returns a LogBuffer keeping at most maxLines lines.

func (*LogBuffer) Lines added in v0.6.0

func (b *LogBuffer) Lines() []string

Lines returns a snapshot of buffered lines, oldest first.

func (*LogBuffer) Write added in v0.6.0

func (b *LogBuffer) Write(p []byte) (int, error)

Write implements io.Writer. Lines are split on '\n'; incomplete lines are buffered until the next Write that completes them.

type Manager

type Manager interface {
	Run(ctx context.Context, fleet *Fleet) error
}

Manager is the orchestration strategy extension point. Implementations load or stream tasks into a Fleet via Add().

type MockAgent

type MockAgent struct {
	// contains filtered or unexported fields
}

MockAgent is an in-memory Agent for tests.

func NewMockAgent

func NewMockAgent() *MockAgent

func (*MockAgent) Done

func (m *MockAgent) Done() <-chan struct{}

func (*MockAgent) Read

func (m *MockAgent) Read(p []byte) (int, error)

func (*MockAgent) ReadInput

func (m *MockAgent) ReadInput(timeout time.Duration) ([]byte, error)

ReadInput reads bytes that were written via Write, blocking up to timeout.

func (*MockAgent) Resize

func (m *MockAgent) Resize(rows, cols int) error

func (*MockAgent) SimulateOutput

func (m *MockAgent) SimulateOutput(data []byte) error

SimulateOutput writes bytes as if the agent process produced them.

func (*MockAgent) Start

func (m *MockAgent) Start(rows, cols int) error

func (*MockAgent) Stop

func (m *MockAgent) Stop() error

func (*MockAgent) Write

func (m *MockAgent) Write(p []byte) (int, error)

type PtyAgent

type PtyAgent struct {
	// contains filtered or unexported fields
}

PtyAgent runs any CLI process inside a PTY.

func NewPtyAgent

func NewPtyAgent(command []string, cfg AgentConfig) *PtyAgent

func (*PtyAgent) Done

func (a *PtyAgent) Done() <-chan struct{}

func (*PtyAgent) Read

func (a *PtyAgent) Read(p []byte) (int, error)

func (*PtyAgent) Resize

func (a *PtyAgent) Resize(rows, cols int) error

func (*PtyAgent) Start

func (a *PtyAgent) Start(rows, cols int) error

func (*PtyAgent) Stop

func (a *PtyAgent) Stop() error

func (*PtyAgent) Write

func (a *PtyAgent) Write(p []byte) (int, error)

type Runner

type Runner struct {
	// contains filtered or unexported fields
}

Runner manages one CLI agent session: starts the PTY, proxies I/O, serves a Unix socket for attach, and maintains a virtual terminal screen for preview. Step injection is not the Runner's concern — callers write to StdinWriter().

func NewRunner

func NewRunner(task Task, ag Agent, cfg FleetConfig, agentCfg AgentConfig) *Runner

func (*Runner) Done

func (r *Runner) Done() <-chan struct{}

func (*Runner) FinishedAt

func (r *Runner) FinishedAt() time.Time

func (*Runner) Lines

func (r *Runner) Lines() []string

Lines returns the current rendered screen of the virtual terminal emulator. All control sequences (backspace, \r overwrite, cursor movement, erase-line) have been applied, so the result matches what a real terminal would display.

func (*Runner) Resize added in v0.2.0

func (r *Runner) Resize(rows, cols int) error

Resize resizes both the underlying PTY agent and the virtual terminal emulator so Lines() keeps mirroring the agent's actual screen. Note the argument order: the PTY/agent take (rows, cols); vt10x takes (cols, rows).

func (*Runner) SetOutput

func (r *Runner) SetOutput(w io.Writer)

SetOutput redirects agent output to w.

func (*Runner) Start

func (r *Runner) Start()

Start launches the PTY session, socket server, and log file. Non-blocking. Safe to call multiple times; only the first call has any effect.

func (*Runner) StartedAt

func (r *Runner) StartedAt() time.Time

func (*Runner) Status

func (r *Runner) Status() Status

func (*Runner) StdinWriter

func (r *Runner) StdinWriter() io.Writer

StdinWriter returns a writer whose bytes are forwarded to the agent's stdin.

func (*Runner) Stop

func (r *Runner) Stop() error

Stop signals the underlying agent to terminate.

func (*Runner) Task

func (r *Runner) Task() Task

type Status

type Status int32

Status represents the lifecycle state of a Runner.

const (
	StatusPending Status = iota
	StatusRunning
	StatusDone
	StatusFailed
)

func (Status) String

func (s Status) String() string

type TUIConfig

type TUIConfig struct {
	Title        func() string           // left side of header; nil = "◈ agentfleet"
	TitleRight   func() string           // right side of header, right-aligned (e.g. connection status)
	RefreshRate  time.Duration           // TUI tick interval          — default: 500ms
	AutoOpen     bool                    // auto-open a tab for each task when it starts — default: true
	MaxDoneTasks int                     // done/failed tasks kept in list; 0 = no limit — default: 10
	Log          *LogBuffer              // nil = no log panel
	OnClose      func(taskID string)     // called when user presses x on a selected task; nil = no-op
	FilterLines  func([]string) []string // pre-process runner output before preview; nil = default chrome filter
}

TUIConfig controls the Bubbletea dashboard appearance.

type Task

type Task interface {
	ID() string
	Name() string
	Command() string
}

Task is the minimum contract for any runnable unit.

Directories

Path Synopsis
examples
file-manager command
file-manager demonstrates loading tasks from a JSON/YAML/Markdown file and running them with the agentfleet TUI and step injection.
file-manager demonstrates loading tasks from a JSON/YAML/Markdown file and running them with the agentfleet TUI and step injection.
generate-manager command
generate-manager calls the Claude API to generate tasks from a natural-language goal, shows the generated tasks for confirmation, then runs them with the TUI.
generate-manager calls the Claude API to generate tasks from a natural-language goal, shows the generated tasks for confirmation, then runs them with the TUI.
http-manager command
http-manager demonstrates loading tasks from an HTTP endpoint.
http-manager demonstrates loading tasks from an HTTP endpoint.
http-manager/taskserver command
taskserver is an example HTTP server that serves task definitions to agentfleet.
taskserver is an example HTTP server that serves task definitions to agentfleet.
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL