Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 8 additions & 0 deletions server/convo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -149,6 +154,9 @@ func (cm *ConversationManager) SetAgentWorking(working bool) {
Model: modelID,
})
}
if !working && onDone != nil {
onDone()
}
}

// IsAgentWorking returns the current agent working state.
Expand Down
65 changes: 65 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down