Add concurrency caps, the two-pane web UI, AGENTS.md, and plan status

This commit is contained in:
Ian Gulliver
2026-10-14 09:40:12 -07:00
parent befd476598
commit e9fd7de556
60 changed files with 5419 additions and 254 deletions
+90 -23
View File
@@ -26,12 +26,14 @@ const (
)
type env struct {
t *testing.T
ctx context.Context
store *store.Store
events chan api.Event
user api.User
task api.Task
t *testing.T
ctx context.Context
store *store.Store
events chan api.Event
user api.User
repo string
project api.Project
task api.Task
}
func newEnv(t *testing.T) *env {
@@ -56,38 +58,45 @@ func newEnv(t *testing.T) *env {
t.Fatalf("update user: %v", err)
}
repo := scratchRepo(t)
p := api.Project{Id: store.NewID(), Name: "demo", RepoPath: repo, DefaultBranch: "main", Remote: "none", CreatedBy: login, CreatedAt: time.Now().UTC()}
err = e.store.CreateProject(ctx, p)
e.repo = scratchRepo(t)
e.project = api.Project{Id: store.NewID(), Name: "demo", RepoPath: e.repo, DefaultBranch: "main", Remote: "none", CreatedBy: login, CreatedAt: time.Now().UTC()}
err = e.store.CreateProject(ctx, e.project)
if err != nil {
t.Fatalf("create project: %v", err)
}
e.task = e.addTask()
return e
}
func (e *env) addTask() api.Task {
e.t.Helper()
id := store.NewID()
e.task = api.Task{
task := api.Task{
Id: id,
ProjectId: p.Id,
ProjectId: e.project.Id,
Title: "test",
Branch: "byre/" + id,
WorktreePath: filepath.Join(t.TempDir(), id),
WorktreePath: filepath.Join(e.t.TempDir(), id),
Status: api.TaskStatusActive,
CreatedBy: login,
CreatedAt: time.Now().UTC(),
}
e.task.BaseRef, err = git.AddWorktree(repo, e.task.WorktreePath, e.task.Branch, "main")
if err != nil {
t.Fatalf("add worktree: %v", err)
}
err = e.store.CreateTask(ctx, e.task)
if err != nil {
t.Fatalf("create task: %v", err)
}
return e
var err error
task.BaseRef, err = git.AddWorktree(e.repo, task.WorktreePath, task.Branch, "main")
if err != nil {
e.t.Fatalf("add worktree: %v", err)
}
err = e.store.CreateTask(e.ctx, task)
if err != nil {
e.t.Fatalf("create task: %v", err)
}
return task
}
func (e *env) manager() *agents.Manager {
m := agents.New(e.store)
m := agents.New(e.store, agents.DefaultLimits)
e.t.Cleanup(m.Close)
return m
}
@@ -221,7 +230,7 @@ func TestInterrupt(t *testing.T) {
func TestRecoverMidTurn(t *testing.T) {
t.Parallel()
e := newEnv(t)
first := agents.New(e.store)
first := agents.New(e.store, agents.DefaultLimits)
sess, err := first.Create(e.ctx, e.task, e.user, "Run the bash command `sleep 15` in the foreground (not in the background). After it finishes, create a file done.txt containing exactly the word done.", testModel)
if err != nil {
@@ -446,6 +455,64 @@ func TestFollowUpQuestionThread(t *testing.T) {
}
}
func TestConcurrencyQueue(t *testing.T) {
t.Parallel()
e := newEnv(t)
m := agents.New(e.store, agents.Limits{Global: 1, PerVendor: map[api.AgentSessionVendor]int{api.Claude: 1}})
t.Cleanup(m.Close)
first, err := m.Create(e.ctx, e.task, e.user, longCommandPrompt, testModel)
if err != nil {
t.Fatalf("create first: %v", err)
}
e.waitFor("first tool call", func(ev api.Event) bool { return ev.Kind == "agent.tool_call" })
second, err := m.Create(e.ctx, e.addTask(), e.user, "Reply with the single word ok.", testModel)
if err != nil {
t.Fatalf("create second: %v", err)
}
if second.State != api.AgentSessionStateQueued {
t.Fatalf("second session is %s with one slot taken, want queued", second.State)
}
usage, err := m.Usage(e.ctx)
if err != nil || usage.Running != 1 || usage.Queued != 1 {
t.Fatalf("usage %+v %v", usage, err)
}
cancelled, err := m.Interrupt(e.ctx, second.Id)
if err != nil || cancelled.State != api.AgentSessionStateIdle {
t.Fatalf("cancel queued turn: %+v %v", cancelled, err)
}
second, err = m.Send(e.ctx, second.Id, login, "Reply with the single word ok.")
if err != nil || second.State != api.AgentSessionStateQueued {
t.Fatalf("requeue: %+v %v", second, err)
}
_, err = m.Interrupt(e.ctx, first.Id)
if err != nil {
t.Fatalf("interrupt first: %v", err)
}
e.waitFor("first idle", func(ev api.Event) bool {
return ev.Kind == "session.state" && *ev.AgentSessionId == first.Id && ev.Payload["state"] == string(api.AgentSessionStateIdle)
})
events := e.waitFor("second idle", func(ev api.Event) bool {
return ev.Kind == "session.state" && *ev.AgentSessionId == second.Id && ev.Payload["state"] == string(api.AgentSessionStateIdle)
})
replied := false
for _, ev := range events {
if ev.Kind == "agent.message" && *ev.AgentSessionId == second.Id && strings.Contains(strings.ToLower(ev.Payload["text"].(string)), "ok") {
replied = true
}
}
if !replied {
t.Fatalf("queued turn never ran: %v", kinds(events))
}
}
func scratchRepo(t *testing.T) string {
dir := t.TempDir()
gitOut(t, dir, "init", "--initial-branch=main")