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.
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.mdpackage 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)
}OpenLogFile returns a rotating io.Writer for the process log stream. Tee it
into whatever handler already feeds the TUI panel and the same lines land on
disk; set TUI.LogPath and the panel divider shows where they went.
cfg := agentfleet.DefaultConfig()
cfg.LogFile.Path = "retask.log" // default: <cwd>/agentfleet.log
logFile, err := agentfleet.OpenLogFile(cfg.LogFile)
if err != nil {
return err
}
defer logFile.Close()
logBuf := agentfleet.NewLogBuffer(500)
cfg.TUI.Log = logBuf
cfg.TUI.LogPath = logFile.Path()
logger := slog.New(slog.NewTextHandler(io.MultiWriter(logBuf, logFile), nil))── Logs (/work/session-abc/retask.log) ───────────────────────────
Rotation follows the Unix convention: the live file keeps its name and older
generations shift down through retask.log.1, retask.log.2, … up to
LogFile.Backups before being discarded.
| Setting | Default | Meaning |
|---|---|---|
LogFile.Enabled |
true |
Write the log stream to a file. false makes OpenLogFile return a nil no-op writer. |
LogFile.Path |
<cwd>/agentfleet.log |
Live log file. Relative paths resolve against the working directory. |
LogFile.MaxBytes |
10MB |
Rotate once the live file exceeds this. 0 disables rotation. |
LogFile.Backups |
5 |
Rotated generations kept. 0 truncates instead of keeping any. |
TUI.ShowLogPath |
true |
Render TUI.LogPath in the log panel divider. |
| 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 |
┌─────────────────────────────────────────────────────────────────┐
│ 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 | Action |
|---|---|
j / k |
Navigate between agents (scroll down/up) |
↵ Enter |
Attach to selected agent's terminal |
q |
Quit (stops all running agents) |
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)
}
}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
}package mytasks
import agentfleet "github.com/hoaitan/agentfleet"
type MyTask struct {
agentfleet.BasicTask
CustomField string
}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
}| 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 |
MIT