diff --git a/AGENTS.md b/AGENTS.md index cd84eb07..c5793c4a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,3 +37,5 @@ reads the `action` field from the input and dispatches to the right sub-component) - `loop/predictable.go` (the "tool smorgasbord" demo response) - See `ui/src/components/AGENTS.md` for more detail. + +16. Prefer `wait: false` for subagents doing background research. Use `wait: true` only when you need the result to continue, and keep timeouts short (30s max). Never block the conversation for minutes. diff --git a/server/convo.go b/server/convo.go index 8a03fa9b..f219adb8 100644 --- a/server/convo.go +++ b/server/convo.go @@ -73,6 +73,10 @@ type ConversationManager struct { // onStateChange is called when the conversation state changes. // This allows the server to broadcast state changes to all subscribers. onStateChange func(state ConversationState) + + // onDone is called when the agent finishes working (transitions to not working). + // Used by subagents to notify their parent conversation. + onDone func() } // NewConversationManager constructs a manager with dependencies but defers hydration until needed. @@ -134,6 +138,7 @@ func (cm *ConversationManager) SetAgentWorking(working bool) { } cm.agentWorking = working onStateChange := cm.onStateChange + onDone := cm.onDone convID := cm.conversationID modelID := cm.modelID cm.mu.Unlock() @@ -149,6 +154,9 @@ func (cm *ConversationManager) SetAgentWorking(working bool) { Model: modelID, }) } + if !working && onDone != nil { + onDone() + } } // IsAgentWorking returns the current agent working state. diff --git a/server/server.go b/server/server.go index e160ca99..cc9bf465 100644 --- a/server/server.go +++ b/server/server.go @@ -837,6 +837,11 @@ func (s *Server) getOrCreateSubagentConversationManager(ctx context.Context, con existing.Touch() return existing, nil } + // Wire up done notification: when this subagent finishes, notify the parent. + manager.onDone = func() { + go s.notifyParentSubagentDone(conversationID) + } + s.activeConversations[conversationID] = manager s.mu.Unlock() return manager, nil @@ -847,6 +852,66 @@ func (s *Server) getOrCreateSubagentConversationManager(ctx context.Context, con return manager, nil } +// notifyParentSubagentDone injects a message into the parent conversation's loop +// when a subagent finishes, so the parent agent knows to check results. +func (s *Server) notifyParentSubagentDone(subagentConversationID string) { + ctx := context.Background() + + // Look up the subagent's parent conversation ID and slug + var conv generated.Conversation + err := s.db.Queries(ctx, func(q *generated.Queries) error { + var err error + conv, err = q.GetConversation(ctx, subagentConversationID) + return err + }) + if err != nil || conv.ParentConversationID == nil { + return + } + + parentID := *conv.ParentConversationID + slug := "unknown" + if conv.Slug != nil { + slug = *conv.Slug + } + + // Find the parent's loop and inject a notification + s.mu.Lock() + parentManager, ok := s.activeConversations[parentID] + s.mu.Unlock() + if !ok { + return + } + + parentManager.mu.Lock() + loopInstance := parentManager.loop + parentManager.mu.Unlock() + if loopInstance == nil { + return + } + + // Get the subagent's last response for the notification + runner := NewSubagentRunner(s) + response, err := runner.getLastAssistantResponse(ctx, subagentConversationID) + if err != nil || response == "" { + response = "(completed, use subagent tool to read results)" + } + // Truncate long responses + if len(response) > 500 { + response = response[:500] + "..." + } + + notification := llm.Message{ + Role: llm.MessageRoleUser, + Content: []llm.Content{{ + Type: llm.ContentTypeText, + Text: fmt.Sprintf("[Subagent '%s' finished]\n%s", slug, response), + }}, + } + + loopInstance.QueueUserMessage(notification) + s.logger.Info("Notified parent of subagent completion", "subagent", slug, "parent", parentID) +} + // ExtractDisplayData extracts display data from message content for storage func ExtractDisplayData(message llm.Message) interface{} { // Build a map of tool_use_id to tool_name for lookups