Add ask_human and the inbox with batched responses and delivery to agents

This commit is contained in:
Ian Gulliver
2026-10-14 07:02:56 -07:00
parent 09951e64e7
commit befd476598
16 changed files with 1962 additions and 129 deletions
+45 -9
View File
@@ -32,15 +32,18 @@ type Manager struct {
mu sync.Mutex
live map[string]*live
closing bool
deliverMu sync.Mutex
}
type live struct {
sess *claude.Session
gate *report.Gate
id string
taskID string
projectID string
stopping bool
sess *claude.Session
gate *report.Gate
id string
taskID string
projectID string
stopping bool
interrupted bool
}
func New(st *store.Store) *Manager {
@@ -119,6 +122,10 @@ func (m *Manager) Interrupt(ctx context.Context, sessionID string) (api.AgentSes
return api.AgentSession{}, fmt.Errorf("%w: session %s has no turn running", ErrConflict, sess.Id)
}
m.mu.Lock()
l.interrupted = true
m.mu.Unlock()
ictx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
@@ -219,7 +226,10 @@ func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task
return api.AgentSession{}, err
}
err = m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateRunning, actor, nil)
err = m.store.TransitionSession(ctx, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor)
if errors.Is(err, store.ErrStateChanged) {
return api.AgentSession{}, fmt.Errorf("%w: %w", ErrConflict, err)
}
if err != nil {
return api.AgentSession{}, err
}
@@ -238,6 +248,10 @@ func (m *Manager) message(ctx context.Context, l *live, actor, text string) erro
return err
}
m.mu.Lock()
l.interrupted = false
m.mu.Unlock()
l.gate.Reset()
return l.sess.Send(text)
}
@@ -266,6 +280,7 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
if err != nil {
slog.Error("record report", "session", l.id, "err", err)
}
m.fileReport(l, r)
}}
opts := claude.Options{
@@ -273,7 +288,7 @@ func (m *Manager) ensureLive(ctx context.Context, sess api.AgentSession, task ap
Model: sess.Model,
GitName: creator.GitName,
GitEmail: creator.GitEmail,
MCPServers: []*mcp.Server{{Name: "byre", Version: "0.0.0", Tools: []mcp.Tool{l.gate.Tool()}}},
MCPServers: []*mcp.Server{{Name: "byre", Version: "0.0.0", Tools: []mcp.Tool{l.gate.Tool(), m.askTool(l)}}},
Hooks: map[string][]claude.HookMatcher{
"Stop": {{Func: l.gate.StopHook}},
},
@@ -330,6 +345,7 @@ func (m *Manager) pump(l *live) {
if err != nil {
slog.Error("record session exit", "session", l.id, "err", err)
}
m.fileError(l, "The agent process exited mid-turn. Send a message to resume it.")
}
func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
@@ -380,7 +396,27 @@ func (m *Manager) handle(ctx context.Context, l *live, ev claude.Event) error {
if err != nil {
return err
}
return m.store.SetSessionState(ctx, l.id, api.AgentSessionStateIdle, agentActor, nil)
m.mu.Lock()
interrupted := l.interrupted
m.mu.Unlock()
if res.IsError && !interrupted {
m.fileError(l, fmt.Sprintf("The turn ended with an error (%s): %s", res.Subtype, res.Result))
}
err = m.store.SetSessionState(ctx, l.id, api.AgentSessionStateIdle, agentActor, nil)
if err != nil {
return err
}
go func() {
err := m.Deliver(context.Background(), l.id)
if err != nil {
slog.Error("deliver inbox responses", "session", l.id, "err", err)
}
}()
return nil
case "system":
if ev.Subtype == "init" {