Enhance Robot Delivery Logic and Context Management

- Introduced manager ID and email fields in the Robot struct to facilitate mandatory delivery to managers.
- Updated delivery routing logic to ensure manager email is always included if manager ID is set, improving delivery reliability.
- Added JSON serialization handling in the Delivery Center to ensure compatibility with various data types.
- Enhanced context ID generation by replacing time-based IDs with NanoID for improved uniqueness and consistency.
This commit is contained in:
Max 2026-01-28 14:44:42 +08:00
parent 235084dbae
commit ecb95f4cc8
7 changed files with 133 additions and 15 deletions

View file

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"sync"
"time"
"github.com/yaoapp/yao/agent/memory"
"github.com/yaoapp/yao/agent/output/message"
@ -411,12 +410,12 @@ func SendInterrupt(contextID string, signal *InterruptSignal) error {
// generateContextID generates a unique context ID
func generateContextID() string {
return fmt.Sprintf("ctx-%d", time.Now().UnixNano())
return message.GenerateNanoID()
}
// RequestID returns the request ID for the context
// RequestID returns a unique request ID using NanoID
func (ctx *Context) RequestID() string {
return fmt.Sprintf("%s", ctx.ID)
return message.GenerateNanoID()
}
// TraceID returns the trace ID for the context

View file

@ -167,6 +167,7 @@ func loadRobotFromDB(memberID string) (*types.Robot, error) {
"id", "member_id", "team_id", "display_name", "bio",
"system_prompt", "robot_status", "autonomous_mode",
"robot_config", "robot_email", "agents", "mcp_servers",
"manager_id",
},
Wheres: []model.QueryWhere{
{Column: "member_id", Value: memberID},

View file

@ -26,6 +26,7 @@ var memberFields = []interface{}{
"robot_email",
"agents",
"mcp_servers",
"manager_id",
}
// SetMemberModel sets the member model name

View file

@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/yaoapp/gou/model"
robottypes "github.com/yaoapp/yao/agent/robot/types"
)
@ -94,20 +95,18 @@ func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Executi
// routeToDeliveryCenter sends content to the Delivery Center for actual delivery
// The Delivery Center decides which channels to use based on robot/user preferences
//
// Delivery logic:
// 1. Manager email: ALWAYS send to manager if manager_id is set (mandatory)
// 2. Additional targets: Append configured email/webhook/process targets
func (e *Executor) routeToDeliveryCenter(ctx *robottypes.Context, exec *robottypes.Execution, robot *robottypes.Robot) error {
if exec.Delivery == nil || exec.Delivery.Content == nil {
return fmt.Errorf("no delivery content to route")
}
// Get delivery preferences from robot config
var prefs *robottypes.DeliveryPreferences
if robot.Config != nil {
prefs = robot.Config.Delivery
}
// If no preferences configured, skip delivery (content is still saved in exec.Delivery)
// Build final delivery preferences by merging manager email + configured targets
prefs := buildDeliveryPreferences(robot)
if prefs == nil || !hasActiveChannels(prefs) {
// No channels configured - mark as success but with no results
exec.Delivery.Success = true
return nil
}
@ -283,6 +282,98 @@ func hasActiveChannels(prefs *robottypes.DeliveryPreferences) bool {
return false
}
// buildDeliveryPreferences builds the final delivery preferences by:
// 1. Always including manager email if manager_id is set (mandatory)
// 2. Appending all configured email/webhook/process targets
func buildDeliveryPreferences(robot *robottypes.Robot) *robottypes.DeliveryPreferences {
if robot == nil {
return nil
}
prefs := &robottypes.DeliveryPreferences{}
// Step 1: Get manager email (mandatory if manager_id is set)
managerEmail := robot.ManagerEmail
if managerEmail == "" && robot.ManagerID != "" {
managerEmail = getManagerEmail(robot.ManagerID)
if managerEmail != "" {
robot.ManagerEmail = managerEmail // Cache for future use
}
}
// Step 2: Build email targets (manager first, then configured targets)
var emailTargets []robottypes.EmailTarget
// Add manager email as first target (mandatory)
if managerEmail != "" {
emailTargets = append(emailTargets, robottypes.EmailTarget{
To: []string{managerEmail},
})
}
// Append configured email targets
if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Email != nil {
for _, target := range robot.Config.Delivery.Email.Targets {
if len(target.To) > 0 {
emailTargets = append(emailTargets, target)
}
}
}
// Set email preference if we have any targets
if len(emailTargets) > 0 {
prefs.Email = &robottypes.EmailPreference{
Enabled: true,
Targets: emailTargets,
}
}
// Step 3: Copy webhook preferences from config (if enabled)
if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Webhook != nil {
if robot.Config.Delivery.Webhook.Enabled && len(robot.Config.Delivery.Webhook.Targets) > 0 {
prefs.Webhook = robot.Config.Delivery.Webhook
}
}
// Step 4: Copy process preferences from config (if enabled)
if robot.Config != nil && robot.Config.Delivery != nil && robot.Config.Delivery.Process != nil {
if robot.Config.Delivery.Process.Enabled && len(robot.Config.Delivery.Process.Targets) > 0 {
prefs.Process = robot.Config.Delivery.Process
}
}
return prefs
}
// getManagerEmail retrieves the manager's email from __yao.member table by member_id
// manager_id in Robot refers to a member_id in __yao.member table
func getManagerEmail(managerID string) string {
if managerID == "" {
return ""
}
m := model.Select("__yao.member")
if m == nil {
return ""
}
records, err := m.Get(model.QueryParam{
Select: []interface{}{"email"},
Wheres: []model.QueryWhere{
{Column: "member_id", Value: managerID},
},
Limit: 1,
})
if err != nil || len(records) == 0 {
return ""
}
if email, ok := records[0]["email"].(string); ok {
return email
}
return ""
}
// FormatDeliveryInput formats the full execution context for the Delivery Agent
func (f *InputFormatter) FormatDeliveryInput(exec *robottypes.Execution, robot *robottypes.Robot) string {
if exec == nil {

View file

@ -295,11 +295,30 @@ func (dc *DeliveryCenter) callProcess(
}
result.Success = true
result.Details = proc.Value
// Convert proc.Value to JSON-serializable format to avoid func type issues
result.Details = toJSONSerializable(proc.Value)
return result
}
// toJSONSerializable ensures the value can be JSON serialized
// Returns the original value if serializable, or a string fallback if not
func toJSONSerializable(v interface{}) interface{} {
if v == nil {
return nil
}
// Try to marshal to check if it's JSON serializable
_, err := json.Marshal(v)
if err != nil {
// If it can't be serialized (e.g., contains func), return a string representation
return fmt.Sprintf("%v", v)
}
// Return original value if it's serializable
return v
}
// buildEmailSubject builds the email subject line
func buildEmailSubject(subject, template string, content *robottypes.DeliveryContent, ctx *robottypes.DeliveryContext) string {
// Use explicit subject if provided

View file

@ -23,6 +23,10 @@ type Robot struct {
AutonomousMode bool `json:"autonomous_mode"`
RobotEmail string `json:"robot_email"` // Robot's email address for sending emails
// Manager info (from __yao.member)
ManagerID string `json:"manager_id"` // Direct manager user_id (who manages this robot)
ManagerEmail string `json:"manager_email"` // Manager's email address (for default delivery)
// Parsed config (from robot_config JSON field)
Config *Config `json:"-"`
@ -393,6 +397,8 @@ func NewRobotFromMap(m map[string]interface{}) (*Robot, error) {
SystemPrompt: getString(m, "system_prompt"),
AutonomousMode: getBool(m, "autonomous_mode"),
RobotEmail: getString(m, "robot_email"),
ManagerID: getString(m, "manager_id"),
ManagerEmail: getString(m, "manager_email"),
}
// Parse robot_status

View file

@ -93,9 +93,10 @@ func (m *manager) addUpdateAndBroadcast(update *types.TraceUpdate) {
// Persist to driver (synchronous - no race)
if err := m.driver.SaveUpdate(context.Background(), m.traceID, update); err != nil {
log.Trace("[MANAGER] addUpdateAndBroadcast: failed to save update type=%s for trace %s: %v", update.Type, m.traceID, err)
} else {
log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID)
}
// else {
// log.Trace("[MANAGER] addUpdateAndBroadcast: successfully saved update type=%s for trace %s", update.Type, m.traceID)
// }
// Add to in-memory history
m.stateAddUpdate(update)