diff --git a/agent/robot/DESIGN.md b/agent/robot/DESIGN.md index 113e9bf1..33739532 100644 --- a/agent/robot/DESIGN.md +++ b/agent/robot/DESIGN.md @@ -698,17 +698,18 @@ Save to KB: ```go type Config struct { - Triggers *Triggers `json:"triggers,omitempty"` - Clock *Clock `json:"clock,omitempty"` - Identity *Identity `json:"identity"` - Quota *Quota `json:"quota"` - KB *KB `json:"kb,omitempty"` // shared KB (same as assistant) - DB *DB `json:"db,omitempty"` // shared DB (same as assistant) - Learn *Learn `json:"learn,omitempty"` // learning for private KB - Resources *Resources `json:"resources"` - Delivery *DeliveryPreferences `json:"delivery,omitempty"` - Events []Event `json:"events,omitempty"` - Executor *Executor `json:"executor,omitempty"` // executor mode settings + Triggers *Triggers `json:"triggers,omitempty"` + Clock *Clock `json:"clock,omitempty"` + Identity *Identity `json:"identity"` + Quota *Quota `json:"quota"` + KB *KB `json:"kb,omitempty"` // shared KB (same as assistant) + DB *DB `json:"db,omitempty"` // shared DB (same as assistant) + Learn *Learn `json:"learn,omitempty"` // learning for private KB + Resources *Resources `json:"resources"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` + Events []Event `json:"events,omitempty"` + Executor *Executor `json:"executor,omitempty"` // executor mode settings + DefaultLocale string `json:"default_locale,omitempty"` // default language for clock/event triggers ("en-US", "zh-CN") } ``` diff --git a/agent/robot/TECHNICAL.md b/agent/robot/TECHNICAL.md index 7067e841..521ba823 100644 --- a/agent/robot/TECHNICAL.md +++ b/agent/robot/TECHNICAL.md @@ -938,16 +938,17 @@ import "time" // Config - robot_config in __yao.member type Config struct { - Triggers *Triggers `json:"triggers,omitempty"` - Clock *Clock `json:"clock,omitempty"` - Identity *Identity `json:"identity"` - Quota *Quota `json:"quota,omitempty"` - KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) - DB *DB `json:"db,omitempty"` // shared database (same as assistant) - Learn *Learn `json:"learn,omitempty"` // learning config for private KB - Resources *Resources `json:"resources,omitempty"` - Delivery *DeliveryPreferences `json:"delivery,omitempty"` // see section 6.2 - Events []Event `json:"events,omitempty"` + Triggers *Triggers `json:"triggers,omitempty"` + Clock *Clock `json:"clock,omitempty"` + Identity *Identity `json:"identity"` + Quota *Quota `json:"quota,omitempty"` + KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) + DB *DB `json:"db,omitempty"` // shared database (same as assistant) + Learn *Learn `json:"learn,omitempty"` // learning config for private KB + Resources *Resources `json:"resources,omitempty"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` // see section 6.2 + Events []Event `json:"events,omitempty"` + DefaultLocale string `json:"default_locale,omitempty"` // Default language for clock/event triggers (e.g., "en-US", "zh-CN") } // Validate validates the config @@ -1238,6 +1239,10 @@ type Execution struct { Phase Phase `json:"phase"` Error string `json:"error,omitempty"` + // UI display fields (updated by executor at each phase) + // These provide human-readable status for frontend display + Name string `json:"name,omitempty"` // Execution title (updated when goals complete) + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description (updated during run phase) // Trigger input (stored for traceability) Input *TriggerInput `json:"input,omitempty"` // original trigger input @@ -1263,6 +1268,7 @@ type TriggerInput struct { Action InterventionAction `json:"action,omitempty"` // task.add, goal.adjust, etc. Messages []context.Message `json:"messages,omitempty"` // user's input (text, images, files) UserID string `json:"user_id,omitempty"` // who triggered + Locale string `json:"locale,omitempty"` // language for UI display (e.g., "en-US", "zh-CN") // For event trigger Source EventSource `json:"source,omitempty"` // webhook | database diff --git a/agent/robot/api/types.go b/agent/robot/api/types.go index 3a990bc6..0f6d55fb 100644 --- a/agent/robot/api/types.go +++ b/agent/robot/api/types.go @@ -64,6 +64,9 @@ type TriggerRequest struct { // Executor mode (optional, overrides robot config) ExecutorMode types.ExecutorMode `json:"executor_mode,omitempty"` + + // i18n support + Locale string `json:"locale,omitempty"` // Locale for UI messages (e.g., "en", "zh") } // InsertPosition - where to insert task in queue diff --git a/agent/robot/executor/standard/executor.go b/agent/robot/executor/standard/executor.go index dd2cb1d0..5f535d63 100644 --- a/agent/robot/executor/standard/executor.go +++ b/agent/robot/executor/standard/executor.go @@ -54,6 +54,7 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri } // Create execution (Job system removed, using ExecutionStore only) + input := types.BuildTriggerInput(trigger, data) exec := &robottypes.Execution{ ID: utils.NewID(), MemberID: robot.MemberID, @@ -62,9 +63,12 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri StartTime: time.Now(), Status: robottypes.ExecPending, Phase: robottypes.AllPhases[startPhaseIndex], - Input: types.BuildTriggerInput(trigger, data), + Input: input, } + // Initialize UI display fields (with i18n support) + exec.Name, exec.CurrentTaskName = e.initUIFields(trigger, input, robot) + // Set robot reference for phase methods exec.SetRobot(robot) @@ -138,12 +142,24 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri return exec, nil } + // Determine locale for UI messages + locale := getEffectiveLocale(robot, exec.Input) + // Execute phases phases := robottypes.AllPhases[startPhaseIndex:] for _, phase := range phases { if err := e.runPhase(ctx, exec, phase, data); err != nil { exec.Status = robottypes.ExecFailed exec.Error = err.Error() + + // Update UI field for failure with i18n + failedPrefix := getLocalizedMessage(locale, "failed_prefix") + failureMsg := failedPrefix + err.Error() + if len(failureMsg) > 100 { + failureMsg = failureMsg[:100] + "..." + } + e.updateUIFields(ctx, exec, "", failureMsg) + log.With(log.F{ "execution_id": exec.ID, "member_id": exec.MemberID, @@ -163,6 +179,9 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri now := time.Now() exec.EndTime = &now + // Update UI field for completion with i18n + e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "completed")) + duration := now.Sub(exec.StartTime) log.With(log.F{ "execution_id": exec.ID, @@ -301,5 +320,154 @@ func (e *Executor) simulateStreamDelay() { time.Sleep(DefaultStreamDelay) } +// initUIFields initializes UI display fields based on trigger type with i18n support +// Returns (name, currentTaskName) +func (e *Executor) initUIFields(trigger robottypes.TriggerType, input *robottypes.TriggerInput, robot *robottypes.Robot) (string, string) { + // Determine locale for UI messages + locale := getEffectiveLocale(robot, input) + + // Get localized default messages + name := getLocalizedMessage(locale, "preparing") + currentTaskName := getLocalizedMessage(locale, "starting") + + switch trigger { + case robottypes.TriggerHuman: + // For human trigger, extract name from first message + if input != nil && len(input.Messages) > 0 { + if content, ok := input.Messages[0].GetContentAsString(); ok && content != "" { + // Use first 100 chars of message as name + name = content + if len(name) > 100 { + name = name[:100] + "..." + } + } + } + case robottypes.TriggerClock: + name = getLocalizedMessage(locale, "scheduled_execution") + case robottypes.TriggerEvent: + if input != nil && input.EventType != "" { + name = getLocalizedMessage(locale, "event_prefix") + input.EventType + } else { + name = getLocalizedMessage(locale, "event_triggered") + } + } + + return name, currentTaskName +} + +// getEffectiveLocale determines the locale for UI display +// Priority: input.Locale > robot.Config.DefaultLocale > "en" +func getEffectiveLocale(robot *robottypes.Robot, input *robottypes.TriggerInput) string { + // 1. Human trigger with explicit locale + if input != nil && input.Locale != "" { + return input.Locale + } + // 2. Robot configured default + if robot != nil && robot.Config != nil { + return robot.Config.GetDefaultLocale() + } + // 3. System default + return "en" +} + +// i18n message maps for UI display fields +// Use simple locale codes (en, zh) as keys +var uiMessages = map[string]map[string]string{ + "en": { + "preparing": "Preparing...", + "starting": "Starting...", + "scheduled_execution": "Scheduled execution", + "event_prefix": "Event: ", + "event_triggered": "Event triggered", + "analyzing_context": "Analyzing context...", + "planning_goals": "Planning goals...", + "breaking_down_tasks": "Breaking down tasks...", + "completed": "Completed", + "failed_prefix": "Failed: ", + "task_prefix": "Task", + }, + "zh": { + "preparing": "准备中...", + "starting": "启动中...", + "scheduled_execution": "定时执行", + "event_prefix": "事件: ", + "event_triggered": "事件触发", + "analyzing_context": "分析上下文...", + "planning_goals": "规划目标...", + "breaking_down_tasks": "分解任务...", + "completed": "已完成", + "failed_prefix": "失败: ", + "task_prefix": "任务", + }, +} + +// getLocalizedMessage returns a localized message for the given key +func getLocalizedMessage(locale string, key string) string { + if messages, ok := uiMessages[locale]; ok { + if msg, ok := messages[key]; ok { + return msg + } + } + // Fallback to English + if messages, ok := uiMessages["en"]; ok { + if msg, ok := messages[key]; ok { + return msg + } + } + return key // Return key as fallback +} + +// updateUIFields updates UI display fields and persists to database +func (e *Executor) updateUIFields(ctx *robottypes.Context, exec *robottypes.Execution, name string, currentTaskName string) { + // Update in-memory execution + if name != "" { + exec.Name = name + } + if currentTaskName != "" { + exec.CurrentTaskName = currentTaskName + } + + // Persist to database + if !e.config.SkipPersistence && e.store != nil { + if err := e.store.UpdateUIFields(ctx.Context, exec.ID, name, currentTaskName); err != nil { + log.With(log.F{ + "execution_id": exec.ID, + "error": err, + }).Warn("Failed to update UI fields: %v", err) + } + } +} + +// extractGoalName extracts the execution name from goals output +func extractGoalName(goals *robottypes.Goals) string { + if goals == nil || goals.Content == "" { + return "" + } + + // Extract first line or first sentence as the goal name + content := goals.Content + // Find first newline + if idx := indexAny(content, "\n\r"); idx > 0 { + content = content[:idx] + } + // Limit length + if len(content) > 150 { + content = content[:150] + "..." + } + return content +} + +// indexAny returns the index of the first occurrence of any char in chars +func indexAny(s string, chars string) int { + for i, c := range s { + for _, ch := range chars { + if c == ch { + return i + } + } + } + return -1 +} + // Verify Executor implements types.Executor var _ types.Executor = (*Executor)(nil) diff --git a/agent/robot/executor/standard/goals.go b/agent/robot/executor/standard/goals.go index 5d531e0f..06b837b0 100644 --- a/agent/robot/executor/standard/goals.go +++ b/agent/robot/executor/standard/goals.go @@ -23,6 +23,10 @@ func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution, return fmt.Errorf("robot not found in execution") } + // Update UI field with i18n + locale := getEffectiveLocale(robot, exec.Input) + e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "planning_goals")) + // Get agent ID for goals phase agentID := "__yao.goals" // default if robot.Config != nil && robot.Config.Resources != nil { @@ -113,6 +117,11 @@ func (e *Executor) RunGoals(ctx *robottypes.Context, exec *robottypes.Execution, return fmt.Errorf("goals agent (%s) returned empty content", agentID) } + // Update Name from goals content (extract first line as execution title) + if goalName := extractGoalName(exec.Goals); goalName != "" { + e.updateUIFields(ctx, exec, goalName, "") + } + return nil } diff --git a/agent/robot/executor/standard/inspiration.go b/agent/robot/executor/standard/inspiration.go index 6354994f..99d24f30 100644 --- a/agent/robot/executor/standard/inspiration.go +++ b/agent/robot/executor/standard/inspiration.go @@ -23,6 +23,10 @@ func (e *Executor) RunInspiration(ctx *robottypes.Context, exec *robottypes.Exec return fmt.Errorf("robot not found in execution") } + // Update UI field with i18n + locale := getEffectiveLocale(robot, exec.Input) + e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "analyzing_context")) + // Build clock context from trigger input or current time var clock *robottypes.ClockContext if exec.Input != nil && exec.Input.Clock != nil { diff --git a/agent/robot/executor/standard/run.go b/agent/robot/executor/standard/run.go index 701f703c..1ca2ff65 100644 --- a/agent/robot/executor/standard/run.go +++ b/agent/robot/executor/standard/run.go @@ -64,6 +64,9 @@ func (e *Executor) RunExecution(ctx *robottypes.Context, exec *robottypes.Execut config = DefaultRunConfig() } + // Determine locale for UI messages + locale := getEffectiveLocale(robot, exec.Input) + // Initialize results slice exec.Results = make([]robottypes.TaskResult, 0, len(exec.Tasks)) @@ -81,6 +84,10 @@ func (e *Executor) RunExecution(ctx *robottypes.Context, exec *robottypes.Execut Progress: fmt.Sprintf("%d/%d tasks", i+1, len(exec.Tasks)), } + // Update UI field with current task description (i18n) + taskName := formatTaskProgressName(task, i, len(exec.Tasks), locale) + e.updateUIFields(ctx, exec, "", taskName) + // Mark task as running task.Status = robottypes.TaskRunning now := time.Now() @@ -122,3 +129,23 @@ func (e *Executor) RunExecution(ctx *robottypes.Context, exec *robottypes.Execut return nil } + +// formatTaskProgressName formats a progress name for the current task (used for UI with i18n) +func formatTaskProgressName(task *robottypes.Task, index int, total int, locale string) string { + taskPrefix := getLocalizedMessage(locale, "task_prefix") + prefix := fmt.Sprintf("%s %d/%d: ", taskPrefix, index+1, total) + + // Try to get description from first message + if len(task.Messages) > 0 { + if content, ok := task.Messages[0].GetContentAsString(); ok && content != "" { + // Truncate if too long + if len(content) > 80 { + content = content[:80] + "..." + } + return prefix + content + } + } + + // Fallback to executor info + return prefix + string(task.ExecutorType) + ":" + task.ExecutorID +} diff --git a/agent/robot/executor/standard/tasks.go b/agent/robot/executor/standard/tasks.go index 41751da5..4e72b753 100644 --- a/agent/robot/executor/standard/tasks.go +++ b/agent/robot/executor/standard/tasks.go @@ -23,6 +23,10 @@ func (e *Executor) RunTasks(ctx *robottypes.Context, exec *robottypes.Execution, return fmt.Errorf("robot not found in execution") } + // Update UI field with i18n + locale := getEffectiveLocale(robot, exec.Input) + e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "breaking_down_tasks")) + // Validate: Goals must exist (from P1) if exec.Goals == nil || exec.Goals.Content == "" { return fmt.Errorf("goals not available for task planning") diff --git a/agent/robot/executor/standard/ui_fields_test.go b/agent/robot/executor/standard/ui_fields_test.go new file mode 100644 index 00000000..bc4f8091 --- /dev/null +++ b/agent/robot/executor/standard/ui_fields_test.go @@ -0,0 +1,356 @@ +package standard + +import ( + "testing" + + "github.com/stretchr/testify/assert" + agentcontext "github.com/yaoapp/yao/agent/context" + robottypes "github.com/yaoapp/yao/agent/robot/types" +) + +// ============================================================================ +// getEffectiveLocale Tests +// ============================================================================ + +func TestGetEffectiveLocale(t *testing.T) { + t.Run("returns_input_locale_when_provided", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{ + DefaultLocale: "en", + }, + } + input := &robottypes.TriggerInput{ + Locale: "zh", + } + + locale := getEffectiveLocale(robot, input) + assert.Equal(t, "zh", locale) + }) + + t.Run("returns_robot_default_locale_when_input_locale_empty", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{ + DefaultLocale: "zh", + }, + } + input := &robottypes.TriggerInput{ + Locale: "", + } + + locale := getEffectiveLocale(robot, input) + assert.Equal(t, "zh", locale) + }) + + t.Run("returns_system_default_when_no_locale_configured", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{}, + } + input := &robottypes.TriggerInput{} + + locale := getEffectiveLocale(robot, input) + assert.Equal(t, "en", locale) + }) + + t.Run("returns_system_default_when_robot_config_nil", func(t *testing.T) { + robot := &robottypes.Robot{} + input := &robottypes.TriggerInput{} + + locale := getEffectiveLocale(robot, input) + assert.Equal(t, "en", locale) + }) + + t.Run("returns_system_default_when_robot_nil", func(t *testing.T) { + input := &robottypes.TriggerInput{} + + locale := getEffectiveLocale(nil, input) + assert.Equal(t, "en", locale) + }) + + t.Run("returns_system_default_when_input_nil", func(t *testing.T) { + robot := &robottypes.Robot{} + + locale := getEffectiveLocale(robot, nil) + assert.Equal(t, "en", locale) + }) +} + +// ============================================================================ +// getLocalizedMessage Tests +// ============================================================================ + +func TestGetLocalizedMessage(t *testing.T) { + t.Run("returns_english_message_for_en_locale", func(t *testing.T) { + msg := getLocalizedMessage("en", "preparing") + assert.Equal(t, "Preparing...", msg) + }) + + t.Run("returns_chinese_message_for_zh_locale", func(t *testing.T) { + msg := getLocalizedMessage("zh", "preparing") + assert.Equal(t, "准备中...", msg) + }) + + t.Run("returns_english_fallback_for_unknown_locale", func(t *testing.T) { + msg := getLocalizedMessage("fr", "preparing") + assert.Equal(t, "Preparing...", msg) + }) + + t.Run("returns_key_for_unknown_message", func(t *testing.T) { + msg := getLocalizedMessage("en", "unknown_key") + assert.Equal(t, "unknown_key", msg) + }) + + t.Run("all_english_messages_exist", func(t *testing.T) { + keys := []string{ + "preparing", "starting", "scheduled_execution", + "event_prefix", "event_triggered", "analyzing_context", + "planning_goals", "breaking_down_tasks", "completed", + "failed_prefix", "task_prefix", + } + for _, key := range keys { + msg := getLocalizedMessage("en", key) + assert.NotEqual(t, key, msg, "English message should exist for key: %s", key) + } + }) + + t.Run("all_chinese_messages_exist", func(t *testing.T) { + keys := []string{ + "preparing", "starting", "scheduled_execution", + "event_prefix", "event_triggered", "analyzing_context", + "planning_goals", "breaking_down_tasks", "completed", + "failed_prefix", "task_prefix", + } + for _, key := range keys { + msg := getLocalizedMessage("zh", key) + assert.NotEqual(t, key, msg, "Chinese message should exist for key: %s", key) + } + }) +} + +// ============================================================================ +// initUIFields Tests +// ============================================================================ + +func TestInitUIFields(t *testing.T) { + executor := New() + + t.Run("human_trigger_extracts_name_from_message", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + input := &robottypes.TriggerInput{ + Messages: []agentcontext.Message{ + {Role: agentcontext.RoleUser, Content: "Please analyze the sales data"}, + }, + } + + name, currentTaskName := executor.initUIFields(robottypes.TriggerHuman, input, robot) + assert.Equal(t, "Please analyze the sales data", name) + assert.Equal(t, "Starting...", currentTaskName) + }) + + t.Run("human_trigger_truncates_long_message", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + longMessage := "This is a very long message that exceeds one hundred characters and should be truncated with an ellipsis at the end" + input := &robottypes.TriggerInput{ + Messages: []agentcontext.Message{ + {Role: agentcontext.RoleUser, Content: longMessage}, + }, + } + + name, _ := executor.initUIFields(robottypes.TriggerHuman, input, robot) + assert.LessOrEqual(t, len(name), 103) // 100 chars + "..." + assert.True(t, len(name) > 100 || name == longMessage[:100]+"...") + }) + + t.Run("clock_trigger_uses_scheduled_execution", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + input := &robottypes.TriggerInput{} + + name, currentTaskName := executor.initUIFields(robottypes.TriggerClock, input, robot) + assert.Equal(t, "Scheduled execution", name) + assert.Equal(t, "Starting...", currentTaskName) + }) + + t.Run("clock_trigger_chinese_locale", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "zh"}, + } + input := &robottypes.TriggerInput{} + + name, currentTaskName := executor.initUIFields(robottypes.TriggerClock, input, robot) + assert.Equal(t, "定时执行", name) + assert.Equal(t, "启动中...", currentTaskName) + }) + + t.Run("event_trigger_with_event_type", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + input := &robottypes.TriggerInput{ + EventType: "lead.created", + } + + name, currentTaskName := executor.initUIFields(robottypes.TriggerEvent, input, robot) + assert.Equal(t, "Event: lead.created", name) + assert.Equal(t, "Starting...", currentTaskName) + }) + + t.Run("event_trigger_without_event_type", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + input := &robottypes.TriggerInput{} + + name, _ := executor.initUIFields(robottypes.TriggerEvent, input, robot) + assert.Equal(t, "Event triggered", name) + }) + + t.Run("event_trigger_chinese_locale", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "zh"}, + } + input := &robottypes.TriggerInput{ + EventType: "order.placed", + } + + name, _ := executor.initUIFields(robottypes.TriggerEvent, input, robot) + assert.Equal(t, "事件: order.placed", name) + }) + + t.Run("input_locale_overrides_robot_default", func(t *testing.T) { + robot := &robottypes.Robot{ + Config: &robottypes.Config{DefaultLocale: "en"}, + } + input := &robottypes.TriggerInput{ + Locale: "zh", + } + + name, currentTaskName := executor.initUIFields(robottypes.TriggerClock, input, robot) + assert.Equal(t, "定时执行", name) + assert.Equal(t, "启动中...", currentTaskName) + }) +} + +// ============================================================================ +// extractGoalName Tests +// ============================================================================ + +func TestExtractGoalName(t *testing.T) { + t.Run("extracts_first_line_from_content", func(t *testing.T) { + goals := &robottypes.Goals{ + Content: "Generate monthly sales report\nAnalyze trends\nSend to stakeholders", + } + + name := extractGoalName(goals) + assert.Equal(t, "Generate monthly sales report", name) + }) + + t.Run("returns_empty_for_nil_goals", func(t *testing.T) { + name := extractGoalName(nil) + assert.Equal(t, "", name) + }) + + t.Run("returns_empty_for_empty_content", func(t *testing.T) { + goals := &robottypes.Goals{ + Content: "", + } + + name := extractGoalName(goals) + assert.Equal(t, "", name) + }) + + t.Run("truncates_long_first_line", func(t *testing.T) { + longLine := "This is an extremely long goal description that exceeds one hundred and fifty characters and should be truncated with an ellipsis at the end to keep the display manageable" + goals := &robottypes.Goals{ + Content: longLine, + } + + name := extractGoalName(goals) + assert.LessOrEqual(t, len(name), 153) // 150 chars + "..." + }) + + t.Run("handles_single_line_content", func(t *testing.T) { + goals := &robottypes.Goals{ + Content: "Single line goal", + } + + name := extractGoalName(goals) + assert.Equal(t, "Single line goal", name) + }) + + t.Run("handles_carriage_return", func(t *testing.T) { + goals := &robottypes.Goals{ + Content: "First goal\r\nSecond goal", + } + + name := extractGoalName(goals) + assert.Equal(t, "First goal", name) + }) +} + +// ============================================================================ +// formatTaskProgressName Tests +// ============================================================================ + +func TestFormatTaskProgressName(t *testing.T) { + t.Run("formats_with_task_description", func(t *testing.T) { + task := &robottypes.Task{ + ID: "task-001", + ExecutorType: robottypes.ExecutorAssistant, + ExecutorID: "analyst", + Messages: []agentcontext.Message{ + {Role: agentcontext.RoleUser, Content: "Analyze sales data"}, + }, + } + + name := formatTaskProgressName(task, 0, 3, "en") + assert.Equal(t, "Task 1/3: Analyze sales data", name) + }) + + t.Run("formats_with_chinese_locale", func(t *testing.T) { + task := &robottypes.Task{ + ID: "task-001", + ExecutorType: robottypes.ExecutorAssistant, + ExecutorID: "analyst", + Messages: []agentcontext.Message{ + {Role: agentcontext.RoleUser, Content: "分析销售数据"}, + }, + } + + name := formatTaskProgressName(task, 1, 5, "zh") + assert.Equal(t, "任务 2/5: 分析销售数据", name) + }) + + t.Run("truncates_long_description", func(t *testing.T) { + longDesc := "This is a very long task description that should be truncated because it exceeds 80 characters which is the maximum length allowed" + task := &robottypes.Task{ + ID: "task-001", + ExecutorType: robottypes.ExecutorAssistant, + ExecutorID: "analyst", + Messages: []agentcontext.Message{ + {Role: agentcontext.RoleUser, Content: longDesc}, + }, + } + + name := formatTaskProgressName(task, 0, 1, "en") + // Should be "Task 1/1: " (11 chars) + truncated content (83 chars max with "...") + assert.Contains(t, name, "...") + assert.LessOrEqual(t, len(name), 100) + }) + + t.Run("fallback_to_executor_info_when_no_messages", func(t *testing.T) { + task := &robottypes.Task{ + ID: "task-001", + ExecutorType: robottypes.ExecutorMCP, + ExecutorID: "calculator", + Messages: []agentcontext.Message{}, + } + + name := formatTaskProgressName(task, 2, 4, "en") + assert.Equal(t, "Task 3/4: mcp:calculator", name) + }) +} diff --git a/agent/robot/store/execution.go b/agent/robot/store/execution.go index adfa16d8..efcaeb15 100644 --- a/agent/robot/store/execution.go +++ b/agent/robot/store/execution.go @@ -25,6 +25,10 @@ type ExecutionRecord struct { Current *CurrentState `json:"current,omitempty"` Error string `json:"error,omitempty"` + // UI display fields (updated by executor at each phase) + Name string `json:"name,omitempty"` // Execution title + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description + // Trigger input Input *types.TriggerInput `json:"input,omitempty"` @@ -313,6 +317,41 @@ func (s *ExecutionStore) UpdateCurrent(ctx context.Context, executionID string, return nil } +// UpdateUIFields updates the UI display fields (name and current_task_name) +// These fields are updated by executor at each phase for frontend display +func (s *ExecutionStore) UpdateUIFields(ctx context.Context, executionID string, name string, currentTaskName string) error { + mod := model.Select(s.modelID) + if mod == nil { + return fmt.Errorf("model %s not found", s.modelID) + } + + updateData := map[string]interface{}{} + if name != "" { + updateData["name"] = name + } + if currentTaskName != "" { + updateData["current_task_name"] = currentTaskName + } + + if len(updateData) == 0 { + return nil // Nothing to update + } + + _, err := mod.UpdateWhere( + model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "execution_id", Value: executionID}, + }, + }, + updateData, + ) + if err != nil { + return fmt.Errorf("failed to update UI fields: %w", err) + } + + return nil +} + // Delete removes an execution record by execution_id func (s *ExecutionStore) Delete(ctx context.Context, executionID string) error { mod := model.Select(s.modelID) @@ -346,6 +385,12 @@ func (s *ExecutionStore) recordToMap(record *ExecutionRecord) map[string]interfa if record.Error != "" { data["error"] = record.Error } + if record.Name != "" { + data["name"] = record.Name + } + if record.CurrentTaskName != "" { + data["current_task_name"] = record.CurrentTaskName + } if record.Current != nil { data["current"] = record.Current } @@ -416,6 +461,12 @@ func (s *ExecutionStore) mapToRecord(row map[string]interface{}) (*ExecutionReco if v, ok := row["error"].(string); ok { record.Error = v } + if v, ok := row["name"].(string); ok { + record.Name = v + } + if v, ok := row["current_task_name"].(string); ok { + record.CurrentTaskName = v + } // JSON fields - need to unmarshal if v := row["current"]; v != nil { @@ -624,20 +675,22 @@ func (s *ExecutionStore) parseTime(v interface{}) *time.Time { // FromExecution creates an ExecutionRecord from a runtime Execution func FromExecution(exec *types.Execution) *ExecutionRecord { record := &ExecutionRecord{ - ExecutionID: exec.ID, - MemberID: exec.MemberID, - TeamID: exec.TeamID, - TriggerType: exec.TriggerType, - Status: exec.Status, - Phase: exec.Phase, - Error: exec.Error, - Input: exec.Input, - Inspiration: exec.Inspiration, - Goals: exec.Goals, - Tasks: exec.Tasks, - Results: exec.Results, - Delivery: exec.Delivery, - Learning: exec.Learning, + ExecutionID: exec.ID, + MemberID: exec.MemberID, + TeamID: exec.TeamID, + TriggerType: exec.TriggerType, + Status: exec.Status, + Phase: exec.Phase, + Error: exec.Error, + Name: exec.Name, + CurrentTaskName: exec.CurrentTaskName, + Input: exec.Input, + Inspiration: exec.Inspiration, + Goals: exec.Goals, + Tasks: exec.Tasks, + Results: exec.Results, + Delivery: exec.Delivery, + Learning: exec.Learning, } // Convert timestamps @@ -662,20 +715,22 @@ func FromExecution(exec *types.Execution) *ExecutionRecord { // ToExecution converts an ExecutionRecord to a runtime Execution func (r *ExecutionRecord) ToExecution() *types.Execution { exec := &types.Execution{ - ID: r.ExecutionID, - MemberID: r.MemberID, - TeamID: r.TeamID, - TriggerType: r.TriggerType, - Status: r.Status, - Phase: r.Phase, - Error: r.Error, - Input: r.Input, - Inspiration: r.Inspiration, - Goals: r.Goals, - Tasks: r.Tasks, - Results: r.Results, - Delivery: r.Delivery, - Learning: r.Learning, + ID: r.ExecutionID, + MemberID: r.MemberID, + TeamID: r.TeamID, + TriggerType: r.TriggerType, + Status: r.Status, + Phase: r.Phase, + Error: r.Error, + Name: r.Name, + CurrentTaskName: r.CurrentTaskName, + Input: r.Input, + Inspiration: r.Inspiration, + Goals: r.Goals, + Tasks: r.Tasks, + Results: r.Results, + Delivery: r.Delivery, + Learning: r.Learning, } // Convert timestamps diff --git a/agent/robot/store/execution_test.go b/agent/robot/store/execution_test.go index 0ded595e..7b0a9134 100644 --- a/agent/robot/store/execution_test.go +++ b/agent/robot/store/execution_test.go @@ -505,6 +505,105 @@ func TestExecutionStoreUpdateCurrent(t *testing.T) { }) } +// TestExecutionStoreUpdateUIFields tests updating UI display fields +func TestExecutionStoreUpdateUIFields(t *testing.T) { + if testing.Short() { + t.Skip("Skipping integration test") + } + + testutils.Prepare(t) + defer testutils.Clean(t) + + cleanupTestExecutions(t) + defer cleanupTestExecutions(t) + + s := store.NewExecutionStore() + ctx := context.Background() + + // Create a base record + startTime := time.Now() + record := &store.ExecutionRecord{ + ExecutionID: "exec_test_uifields_001", + MemberID: "member_uifields_001", + TeamID: "team_uifields_001", + TriggerType: types.TriggerHuman, + Status: types.ExecRunning, + Phase: types.PhaseInspiration, + StartTime: &startTime, + } + err := s.Save(ctx, record) + require.NoError(t, err) + + t.Run("updates_name_only", func(t *testing.T) { + err := s.UpdateUIFields(ctx, "exec_test_uifields_001", "Analyze sales data", "") + require.NoError(t, err) + + saved, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, "Analyze sales data", saved.Name) + assert.Equal(t, "", saved.CurrentTaskName) + }) + + t.Run("updates_current_task_name_only", func(t *testing.T) { + err := s.UpdateUIFields(ctx, "exec_test_uifields_001", "", "Analyzing context...") + require.NoError(t, err) + + saved, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, "Analyze sales data", saved.Name) // Previous value retained + assert.Equal(t, "Analyzing context...", saved.CurrentTaskName) + }) + + t.Run("updates_both_fields", func(t *testing.T) { + err := s.UpdateUIFields(ctx, "exec_test_uifields_001", "Generate monthly report", "Task 1/3: Collect data") + require.NoError(t, err) + + saved, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, "Generate monthly report", saved.Name) + assert.Equal(t, "Task 1/3: Collect data", saved.CurrentTaskName) + }) + + t.Run("does_nothing_when_both_empty", func(t *testing.T) { + // Get current values + before, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + + // Update with empty strings + err = s.UpdateUIFields(ctx, "exec_test_uifields_001", "", "") + require.NoError(t, err) + + // Values should remain unchanged + after, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, before.Name, after.Name) + assert.Equal(t, before.CurrentTaskName, after.CurrentTaskName) + }) + + t.Run("handles_chinese_content", func(t *testing.T) { + err := s.UpdateUIFields(ctx, "exec_test_uifields_001", "生成月度报告", "任务 2/3: 分析数据") + require.NoError(t, err) + + saved, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, "生成月度报告", saved.Name) + assert.Equal(t, "任务 2/3: 分析数据", saved.CurrentTaskName) + }) + + t.Run("handles_long_content", func(t *testing.T) { + longName := "This is a very long execution name that might come from a detailed user instruction about what they want the robot to accomplish in this particular run cycle" + longTask := "Task 1/5: Processing a complex multi-step operation with various sub-tasks that need to be completed..." + + err := s.UpdateUIFields(ctx, "exec_test_uifields_001", longName, longTask) + require.NoError(t, err) + + saved, err := s.Get(ctx, "exec_test_uifields_001") + require.NoError(t, err) + assert.Equal(t, longName, saved.Name) + assert.Equal(t, longTask, saved.CurrentTaskName) + }) +} + // TestExecutionStoreDelete tests deleting execution records func TestExecutionStoreDelete(t *testing.T) { if testing.Short() { @@ -569,17 +668,19 @@ func TestExecutionRecordConversion(t *testing.T) { now := time.Now() endTime := now.Add(time.Hour) exec := &types.Execution{ - ID: "exec_convert_001", - MemberID: "member_convert_001", - TeamID: "team_convert_001", - TriggerType: types.TriggerHuman, - Status: types.ExecCompleted, - Phase: types.PhaseDelivery, - StartTime: now, - EndTime: &endTime, - Error: "", - Inspiration: &types.InspirationReport{Content: "Test inspiration"}, - Goals: &types.Goals{Content: "Test goals"}, + ID: "exec_convert_001", + MemberID: "member_convert_001", + TeamID: "team_convert_001", + TriggerType: types.TriggerHuman, + Status: types.ExecCompleted, + Phase: types.PhaseDelivery, + StartTime: now, + EndTime: &endTime, + Error: "", + Name: "Analyze sales data", + CurrentTaskName: "Task 1/3: Processing", + Inspiration: &types.InspirationReport{Content: "Test inspiration"}, + Goals: &types.Goals{Content: "Test goals"}, Tasks: []types.Task{ {ID: "task_001", ExecutorType: types.ExecutorAssistant}, }, @@ -608,22 +709,27 @@ func TestExecutionRecordConversion(t *testing.T) { assert.Len(t, record.Results, 1) assert.NotNil(t, record.Current) assert.Equal(t, 1, record.Current.TaskIndex) + // Verify UI fields conversion + assert.Equal(t, "Analyze sales data", record.Name) + assert.Equal(t, "Task 1/3: Processing", record.CurrentTaskName) }) t.Run("converts_to_execution", func(t *testing.T) { now := time.Now() endTime := now.Add(time.Hour) record := &store.ExecutionRecord{ - ExecutionID: "exec_convert_002", - MemberID: "member_convert_002", - TeamID: "team_convert_002", - TriggerType: types.TriggerClock, - Status: types.ExecRunning, - Phase: types.PhaseRun, - StartTime: &now, - EndTime: &endTime, - Inspiration: &types.InspirationReport{Content: "Test inspiration"}, - Goals: &types.Goals{Content: "Test goals"}, + ExecutionID: "exec_convert_002", + MemberID: "member_convert_002", + TeamID: "team_convert_002", + TriggerType: types.TriggerClock, + Status: types.ExecRunning, + Phase: types.PhaseRun, + StartTime: &now, + EndTime: &endTime, + Name: "定时执行", + CurrentTaskName: "任务 1/2: 数据分析", + Inspiration: &types.InspirationReport{Content: "Test inspiration"}, + Goals: &types.Goals{Content: "Test goals"}, Tasks: []types.Task{ {ID: "task_002", ExecutorType: types.ExecutorProcess}, }, @@ -650,6 +756,9 @@ func TestExecutionRecordConversion(t *testing.T) { assert.Len(t, exec.Results, 1) assert.NotNil(t, exec.Current) assert.Equal(t, 0, exec.Current.TaskIndex) + // Verify UI fields conversion + assert.Equal(t, "定时执行", exec.Name) + assert.Equal(t, "任务 1/2: 数据分析", exec.CurrentTaskName) }) } diff --git a/agent/robot/types/config.go b/agent/robot/types/config.go index 78bbb656..942eb87a 100644 --- a/agent/robot/types/config.go +++ b/agent/robot/types/config.go @@ -7,17 +7,18 @@ import ( // Config - robot_config in __yao.member type Config struct { - Triggers *Triggers `json:"triggers,omitempty"` - Clock *Clock `json:"clock,omitempty"` - Identity *Identity `json:"identity"` - Quota *Quota `json:"quota,omitempty"` - KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) - DB *DB `json:"db,omitempty"` // shared database (same as assistant) - Learn *Learn `json:"learn,omitempty"` // learning config for private KB - Resources *Resources `json:"resources,omitempty"` - Delivery *DeliveryPreferences `json:"delivery,omitempty"` // delivery preferences (see robot.go) - Events []Event `json:"events,omitempty"` - Executor *ExecutorConfig `json:"executor,omitempty"` // executor mode settings + Triggers *Triggers `json:"triggers,omitempty"` + Clock *Clock `json:"clock,omitempty"` + Identity *Identity `json:"identity"` + Quota *Quota `json:"quota,omitempty"` + KB *KB `json:"kb,omitempty"` // shared knowledge base (same as assistant) + DB *DB `json:"db,omitempty"` // shared database (same as assistant) + Learn *Learn `json:"learn,omitempty"` // learning config for private KB + Resources *Resources `json:"resources,omitempty"` + Delivery *DeliveryPreferences `json:"delivery,omitempty"` // delivery preferences (see robot.go) + Events []Event `json:"events,omitempty"` + Executor *ExecutorConfig `json:"executor,omitempty"` // executor mode settings + DefaultLocale string `json:"default_locale,omitempty"` // default language for clock/event triggers ("en", "zh") } // ExecutorConfig - executor settings @@ -59,6 +60,14 @@ func (c *Config) Validate() error { return nil } +// GetDefaultLocale returns the default locale (default: "en") +func (c *Config) GetDefaultLocale() string { + if c == nil || c.DefaultLocale == "" { + return "en" + } + return c.DefaultLocale +} + // Triggers - trigger enable/disable type Triggers struct { Clock *TriggerSwitch `json:"clock,omitempty"` diff --git a/agent/robot/types/robot.go b/agent/robot/types/robot.go index 1dfea09a..57bae6d7 100644 --- a/agent/robot/types/robot.go +++ b/agent/robot/types/robot.go @@ -129,6 +129,11 @@ type Execution struct { Phase Phase `json:"phase"` Error string `json:"error,omitempty"` + // UI display fields (updated by executor at each phase) + // These provide human-readable status for frontend display + Name string `json:"name,omitempty"` // Execution title (updated when goals complete) + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description (updated during run phase) + // Trigger input (stored for traceability) Input *TriggerInput `json:"input,omitempty"` // original trigger input @@ -163,6 +168,7 @@ type TriggerInput struct { Action InterventionAction `json:"action,omitempty"` // task.add, goal.adjust, etc. Messages []agentcontext.Message `json:"messages,omitempty"` // user's input (text, images, files) UserID string `json:"user_id,omitempty"` // who triggered + Locale string `json:"locale,omitempty"` // language for UI display (e.g., "en-US", "zh-CN") // For event trigger Source EventSource `json:"source,omitempty"` // webhook | database diff --git a/data/bindata.go b/data/bindata.go index c5b0e30e..018d2494 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -346,7 +346,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -366,7 +366,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -386,7 +386,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -406,7 +406,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -426,7 +426,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -446,7 +446,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -466,7 +466,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -486,7 +486,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -506,7 +506,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -526,7 +526,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -546,7 +546,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -566,7 +566,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -586,7 +586,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -606,7 +606,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -626,7 +626,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -646,7 +646,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -666,7 +666,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -686,7 +686,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -706,7 +706,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -726,7 +726,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -746,7 +746,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -766,7 +766,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -786,7 +786,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -806,7 +806,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -826,7 +826,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -846,7 +846,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -866,7 +866,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -886,7 +886,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -906,7 +906,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -926,7 +926,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -946,7 +946,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -966,7 +966,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -986,7 +986,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1006,7 +1006,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1026,7 +1026,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1046,7 +1046,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1066,7 +1066,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1086,7 +1086,7 @@ func initFlowsMenuFlowYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1106,7 +1106,7 @@ func initFormsAccountFormYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1126,7 +1126,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1146,7 +1146,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1166,7 +1166,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1186,7 +1186,7 @@ func initLoginsAdminLoginYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1206,7 +1206,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1226,7 +1226,7 @@ func initModelsAdminUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1246,7 +1246,7 @@ func initModelsTestsPetModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1266,7 +1266,7 @@ func initNeoNeoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1286,7 +1286,7 @@ func initPublicReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1306,7 +1306,7 @@ func initPublicAssetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1326,7 +1326,7 @@ func initPublicAssetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1346,7 +1346,7 @@ func initPublicAssetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1366,7 +1366,7 @@ func initPublicAssetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1386,7 +1386,7 @@ func initPublicAssetsLibsuiMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1406,7 +1406,7 @@ func initPublicAssetsLibsuiMinJsMap() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1426,7 +1426,7 @@ func initPublicIndexCfg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1446,7 +1446,7 @@ func initPublicIndexSui() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1466,7 +1466,7 @@ func initScriptsAccountTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1486,7 +1486,7 @@ func initScriptsAiNeoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1506,7 +1506,7 @@ func initScriptsTestsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1526,7 +1526,7 @@ func initScriptsUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1546,7 +1546,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1566,7 +1566,7 @@ func initTablesAccountTabYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1586,7 +1586,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1606,7 +1606,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13047, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13047, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1626,7 +1626,7 @@ func libsuiOpenapiTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/openapi.ts", size: 22959, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "libsui/openapi.ts", size: 22959, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1646,7 +1646,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1666,7 +1666,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1686,7 +1686,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1706,7 +1706,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1726,7 +1726,7 @@ func yaoAssistantsEntityPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/package.yao", size: 212, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1746,7 +1746,7 @@ func yaoAssistantsEntityPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/entity/prompts.yml", size: 930, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1766,7 +1766,7 @@ func yaoAssistantsKeywordPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/package.yao", size: 200, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1786,7 +1786,7 @@ func yaoAssistantsKeywordPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 990, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/prompts.yml", size: 990, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1806,7 +1806,7 @@ func yaoAssistantsKeywordSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 4104, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/keyword/src/index.ts", size: 4104, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1826,7 +1826,7 @@ func yaoAssistantsNeedsearchPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/package.yao", size: 207, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1846,7 +1846,7 @@ func yaoAssistantsNeedsearchPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 3092, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/prompts.yml", size: 3092, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1866,7 +1866,7 @@ func yaoAssistantsNeedsearchSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/needsearch/src/index.ts", size: 2767, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1886,7 +1886,7 @@ func yaoAssistantsPromptPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/package.yao", size: 186, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1906,7 +1906,7 @@ func yaoAssistantsPromptPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/prompt/prompts.yml", size: 621, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1926,7 +1926,7 @@ func yaoAssistantsQuerydslPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/package.yao", size: 196, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1946,7 +1946,7 @@ func yaoAssistantsQuerydslPromptsAggregationYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/aggregation.yml", size: 6982, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/aggregation.yml", size: 6982, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1966,7 +1966,7 @@ func yaoAssistantsQuerydslPromptsComplexYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/complex.yml", size: 7352, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/complex.yml", size: 7352, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1986,7 +1986,7 @@ func yaoAssistantsQuerydslPromptsFilterYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/filter.yml", size: 7087, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/filter.yml", size: 7087, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2006,7 +2006,7 @@ func yaoAssistantsQuerydslPromptsJoinYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/join.yml", size: 8167, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts/join.yml", size: 8167, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2026,7 +2026,7 @@ func yaoAssistantsQuerydslPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 5836, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/prompts.yml", size: 5836, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2046,7 +2046,7 @@ func yaoAssistantsQuerydslSrcIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/querydsl/src/index.ts", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/querydsl/src/index.ts", size: 1873, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2066,7 +2066,7 @@ func yaoAssistantsRobot_promptPackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/robot_prompt/package.yao", size: 204, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/robot_prompt/package.yao", size: 204, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2086,7 +2086,7 @@ func yaoAssistantsRobot_promptPromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/robot_prompt/prompts.yml", size: 2606, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/robot_prompt/prompts.yml", size: 2606, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2106,7 +2106,7 @@ func yaoAssistantsTitlePackageYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/title/package.yao", size: 178, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2126,7 +2126,7 @@ func yaoAssistantsTitlePromptsYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/assistants/title/prompts.yml", size: 958, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2146,7 +2146,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2166,7 +2166,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2186,7 +2186,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2206,7 +2206,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2226,7 +2226,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2246,7 +2246,7 @@ func yaoDataKbProvidersChunkingSemanticEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2266,7 +2266,7 @@ func yaoDataKbProvidersChunkingSemanticZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2286,7 +2286,7 @@ func yaoDataKbProvidersChunkingStructuredEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2306,7 +2306,7 @@ func yaoDataKbProvidersChunkingStructuredZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2326,7 +2326,7 @@ func yaoDataKbProvidersConverterMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2346,7 +2346,7 @@ func yaoDataKbProvidersConverterMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2366,7 +2366,7 @@ func yaoDataKbProvidersConverterOcrEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2386,7 +2386,7 @@ func yaoDataKbProvidersConverterOcrZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2406,7 +2406,7 @@ func yaoDataKbProvidersConverterOfficeEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2426,7 +2426,7 @@ func yaoDataKbProvidersConverterOfficeZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2446,7 +2446,7 @@ func yaoDataKbProvidersConverterUtf8EnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2466,7 +2466,7 @@ func yaoDataKbProvidersConverterUtf8ZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2486,7 +2486,7 @@ func yaoDataKbProvidersConverterVideoEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2506,7 +2506,7 @@ func yaoDataKbProvidersConverterVideoZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2526,7 +2526,7 @@ func yaoDataKbProvidersConverterVisionEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2546,7 +2546,7 @@ func yaoDataKbProvidersConverterVisionZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2566,7 +2566,7 @@ func yaoDataKbProvidersConverterWhisperEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2586,7 +2586,7 @@ func yaoDataKbProvidersConverterWhisperZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2606,7 +2606,7 @@ func yaoDataKbProvidersEmbeddingFastembedEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2626,7 +2626,7 @@ func yaoDataKbProvidersEmbeddingFastembedZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2646,7 +2646,7 @@ func yaoDataKbProvidersEmbeddingOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2666,7 +2666,7 @@ func yaoDataKbProvidersEmbeddingOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2686,7 +2686,7 @@ func yaoDataKbProvidersExtractionOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2706,7 +2706,7 @@ func yaoDataKbProvidersExtractionOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2726,7 +2726,7 @@ func yaoDataKbProvidersFetcherHttpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2746,7 +2746,7 @@ func yaoDataKbProvidersFetcherHttpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2766,7 +2766,7 @@ func yaoDataKbProvidersFetcherMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2786,7 +2786,7 @@ func yaoDataKbProvidersFetcherMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2806,7 +2806,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2826,7 +2826,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2846,7 +2846,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2866,7 +2866,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2886,7 +2886,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2906,7 +2906,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2926,7 +2926,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2946,7 +2946,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2966,7 +2966,7 @@ func yaoModelsAgentAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6945, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2986,12 +2986,12 @@ func yaoModelsAgentChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 3093, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _yaoModelsAgentExecutionModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\x4d\x6f\xdc\x36\x10\xbd\xfb\x57\x0c\x74\x72\x00\x27\x48\xdb\xa4\x80\x7d\x2b\x1a\xa3\x75\x91\x22\x86\xeb\x9e\x0c\x63\xc1\x95\x66\xb5\xb4\xf9\xa1\x0c\x47\xb1\x05\xc3\xff\x3d\x20\xa5\x95\x28\xad\x76\x23\x6b\x9d\x4b\xa2\x1d\x0e\x87\xef\xbd\x21\x87\x1c\x3f\x1d\x01\x24\x46\x68\x4c\xce\x20\x39\x7f\xc4\xb4\x64\x69\x4d\x72\xe2\xcd\x4a\x2c\x51\x79\xfb\x95\x5d\x5a\x86\xc1\x68\x86\x2e\x25\x59\x04\x43\xeb\x83\x1b\x1f\x58\x4b\xc7\x96\x2a\x58\x59\x02\x26\x91\xde\x4b\x93\xc3\xe5\xfb\xb7\x97\x1f\xa1\x58\x0b\x87\x60\x4b\x2e\x4a\x76\x20\x4c\x06\x8e\x05\x97\xae\x8e\xcb\x22\x77\xc9\x19\xdc\x24\x22\x47\xc3\xc9\x09\x24\xe4\x43\xfb\x0f\x57\x39\x46\x9d\xdc\x06\xbf\x65\x29\x15\x4b\xbf\x36\x53\x89\xc1\x44\x28\x32\x6b\x54\x95\x9c\xc1\x4a\x28\x57\x1b\x9d\x25\x4e\xce\xe0\xf4\xf4\xf4\xb4\x89\xbf\x54\x9e\xed\x53\xc7\x3b\xac\xb4\xc0\x8e\x1f\x24\xa9\xd5\xda\x2f\xbf\x87\x59\x1d\x08\x9e\x43\xd4\xd4\xaa\x52\x9b\x00\xfc\x08\x00\xe0\x29\xfc\x1b\x69\x2b\xb3\x40\x2f\xd8\xb8\x2a\x82\xed\xe2\x53\x67\x6b\xc5\x8e\x8d\x11\x8a\x3f\x4a\xb6\x6f\xa5\x49\x09\xbd\x05\x0a\x92\x5a\x50\x05\xf7\x58\x25\xc1\xfb\xf9\x64\x7c\xdd\x16\xf7\x62\x0c\x81\x63\x92\x26\x1f\x41\xd1\x26\x1b\x76\xe0\xf9\xdf\xc8\xaf\x25\x46\xb2\xc8\x0c\x0d\xcb\x95\x44\x8a\xc2\xa1\xc9\x79\x9d\x9c\xc1\xef\x1f\x5a\x9b\x29\x95\x6a\x52\xd0\x26\x29\x0c\x94\x21\x62\x94\xcf\x60\x95\x26\xc3\xc7\xc6\xb8\x97\xa8\x46\xbd\x44\x7a\x19\xcb\x7f\xc3\x9c\x5d\x14\xeb\xc4\xeb\x8d\x0f\x1c\x97\x0e\xa9\xe1\xc9\x15\xac\xc8\x6a\x58\x2c\x2a\x61\xdf\xd5\x3e\x6f\x66\x12\x9f\x4c\x91\x51\xe8\x97\x11\xbc\x46\xa1\x77\xd1\x6b\xc6\x80\xd7\x08\xe1\x88\xc1\x12\x95\x35\xb9\x03\xb6\x3f\x9b\xc9\x9d\x5d\xbe\x8c\xc8\x3f\x76\xb9\x8b\xc7\x67\x69\xee\x31\x83\x3b\xbb\x7c\x57\x7b\x85\x9a\xa3\xad\x91\x6c\x07\xd1\xf6\x73\x99\xb7\xef\x98\x64\x9e\x23\x2d\x02\x85\x2d\x42\x68\x4a\x3d\x96\x97\x7a\x12\x5c\xf7\x26\x45\xa4\xfe\xb6\x0f\xc0\x6b\xe9\xa2\x03\xf6\x20\x1c\x34\x8b\x61\x24\x9d\xdd\x94\xe0\x9b\x24\x55\x36\xbd\xf7\xd5\x6b\x5d\x6a\x11\xca\x18\x7e\xf3\xf1\x6e\x5f\x31\x73\x51\xa9\x9e\x42\xf4\xbf\x81\x7b\x44\xb1\x2b\x31\xc3\x98\x11\xa5\x02\x4d\x16\x72\x08\x09\x95\xc6\x34\x9f\xa9\xd5\x85\x42\xf6\x32\x40\xb2\x12\x52\xd5\x5f\xa9\x30\x29\x2a\xff\xa3\x63\x9c\xe1\x4a\x94\x2a\x2c\xd8\xc6\x7a\x3d\x35\xc2\x55\x36\x59\x8c\xcb\xbe\x77\xa4\xc5\x9f\x25\x91\x2f\xeb\x5d\xb6\x07\x91\x23\x49\xa4\x71\x85\x24\xb1\xb9\xa9\x72\x2b\x94\xf3\x1f\x2c\xdc\xbd\x6b\x84\xf2\xff\x65\xa8\xe4\x37\xa4\xca\x7f\x2b\x14\x14\xc4\x1b\x15\xa6\x17\xf1\xf5\xc4\x49\x6b\x52\xdb\xf2\xdc\xb9\x78\xa1\x56\x9e\x8d\x08\x7e\xcf\x4c\x92\xc9\xe4\x61\xeb\x20\x1c\x7b\xee\x8b\x00\xec\x04\x0a\xb2\x39\xa1\x73\x6f\x46\xb9\xfc\x10\x35\x12\x59\xda\xc6\xcc\xf8\xc8\x63\xf7\x64\xdf\x3b\xde\xde\x7e\x04\x34\x3a\x27\x72\x04\xb9\x8a\x72\xbb\xd9\xb1\x73\xe0\x49\x53\x94\xd3\x25\xbd\xe8\x7b\x47\xf0\xbe\x90\xcc\xa5\x11\x6a\x53\x52\x20\x04\x86\xe3\xa6\x32\x85\x89\x33\x15\x1c\xdd\x4e\x3f\x04\x3a\x32\x27\x82\x7b\xf9\xbe\x79\x30\xc2\x71\xe4\x7a\x85\x85\xa5\xb9\x30\x9b\x83\x33\x11\xe0\x5f\x7d\xef\x18\xda\x2f\x2d\xb4\xe0\x34\x13\x4e\x73\x7c\x27\xc2\xb9\xee\x7b\xc7\x70\x7e\x6d\xe1\xdc\xdc\x7a\xb7\x99\x78\x08\x5d\xa9\x78\x3a\xa2\xab\xa1\x7f\x8c\xe9\xb7\x01\xa6\xda\x79\x26\xb2\xae\xb0\x4d\x84\xf6\x69\x6b\x42\x8c\xed\x43\x8b\x6d\xe3\x77\x10\xba\xb6\xd4\x4e\x45\xf7\x79\x6b\x42\x8c\xee\x63\xa4\xdc\xc6\xf3\xdc\x30\x55\x33\xe1\x39\x16\xc4\x0b\x96\x7a\xe4\xda\xf2\x56\xc7\x42\x17\xe3\x17\x39\x31\x5c\xf7\xe6\xed\xba\xcc\x89\x61\x24\xd4\xc1\x2f\x2d\x34\xd9\x0c\xe0\xe7\x26\x9b\x02\x1b\x4d\x76\x18\xe8\x23\x80\xdb\xa6\x0b\x55\xa1\x38\xf9\x66\xb0\xe6\xd0\xf4\x26\xed\xef\x08\xf9\x5a\xb8\x2f\x26\xc2\xa6\x6d\x56\xc3\x8e\x1b\x8b\x6e\xd8\xb7\x7b\xe3\xbd\xce\xca\x12\xca\xdc\xf4\x47\x5b\x64\x75\x93\x1a\x40\xe3\xde\x26\xf5\x71\x31\x68\x86\x17\x4d\xb4\xe1\x1b\x2d\x6a\x78\x63\x38\xed\x03\xf1\x76\x2b\x49\xb5\x64\x63\x59\xb8\xf0\x23\xf5\xab\xbd\x6e\xb6\xba\xab\xf2\x6b\x89\x24\xd1\xc1\x83\xe4\x75\xf3\x4e\x84\x95\x54\x8c\xb4\xbf\xed\x1d\x63\x12\xda\xa7\x7d\x3c\xda\xfe\xea\x40\x16\x3e\xce\xcf\xe2\xd0\x74\x1b\xe1\x94\xed\x60\xd1\x6b\x48\x7a\x27\x7e\x26\x9d\xe6\x85\xe0\xe7\x80\x30\x42\x55\x4e\xba\x97\x43\xef\x36\xd2\x2e\xe4\xc3\x7d\x74\x18\x6c\xda\xf1\x77\x9b\x65\xd5\x6c\xb3\x64\x70\x70\xdb\x37\xf6\x53\x54\x51\xdc\xe6\xe4\x43\xe2\xec\x8a\x17\x19\xfa\xc6\xc3\xb5\xaf\x62\xdf\x56\x90\x96\xce\xd5\x53\xbd\x2b\x3c\x1f\x3d\x1f\x7d\x0f\x00\x00\xff\xff\x5b\x06\x59\x7f\x59\x13\x00\x00") +var _yaoModelsAgentExecutionModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x58\x51\x4f\xdc\x38\x10\x7e\xe7\x57\x8c\xf2\x44\x25\x5a\xb5\xbd\x72\x12\xbc\x9d\xae\xe8\x8e\x53\xef\x8a\x38\xfa\x84\xd0\xca\x49\x66\xb3\x66\x1d\x3b\xb5\xc7\x85\x08\xf1\xdf\x2b\x3b\x4e\xd6\xc9\x66\x97\x10\xe8\x0b\x64\xc7\xe3\xf1\xf7\xcd\x8c\xc7\x1e\x3f\x1c\x00\x24\x92\x95\x98\x9c\x42\x72\x76\x8f\x99\x25\xae\x64\x72\xe4\xc4\x82\xa5\x28\x9c\xfc\x52\xa5\x8a\x60\x30\x9a\xa3\xc9\x34\xaf\xbc\xa0\xd3\xc1\x56\x07\x56\xdc\x90\xd2\x35\x2c\x95\x06\xd2\x2c\x5b\x73\x59\xc0\xc5\xfb\xb7\x17\xc7\x50\xad\x98\x41\x50\x96\x2a\x4b\x06\x98\xcc\xc1\x10\x23\x6b\x1a\xbb\xc4\x0a\x93\x9c\xc2\x75\xc2\x0a\x94\x94\x1c\x41\xa2\x9d\x69\xf7\x61\x6a\x43\x58\x26\x37\x5e\x2f\xb5\x5c\x10\x77\x6b\x93\xb6\xe8\x45\x1a\x59\xae\xa4\xa8\x93\x53\x58\x32\x61\x1a\xa1\x51\x9a\x92\x53\x38\x39\x39\x39\x09\xf6\x53\xe1\xd8\x3e\x6c\x78\xfb\x95\x16\xb8\xe1\x07\x49\xa6\xca\xd2\x2d\xbf\x87\x59\x63\x08\x1e\xbd\xd5\x4c\x09\x5b\x4a\x0f\xfc\x00\x00\xe0\xc1\xff\x8d\x7c\xcb\x73\x4f\xcf\xcb\xa8\xae\xbc\xec\xfc\xf3\x46\xd6\x39\x3b\x16\x46\x28\xfe\xb0\xa4\xde\x72\x99\x69\x74\x12\xa8\x34\x2f\x99\xae\x61\x8d\x75\xe2\xb5\x1f\x8f\xc6\xd7\xed\x70\x2f\xc6\x10\x18\xd2\x5c\x16\x23\x28\xba\x60\xc3\x0e\x3c\xdf\x24\xff\x6e\x31\x72\x0b\xcf\x51\x12\x5f\x72\xd4\x91\x39\x94\x05\xad\x92\x53\xf8\xfd\x53\x27\x93\x56\x88\x10\x82\x2e\x48\x7e\xc0\x7a\x8b\x51\x3c\xbd\x94\xcb\x1c\xef\x83\x70\x2f\xd1\x12\xcb\x14\xf5\xf3\x58\xfe\xeb\xe7\xec\xa2\xd8\x04\xbe\x6c\x75\xe0\xd0\x1a\xd4\x81\x27\xd5\xb0\xd4\xaa\x84\xc5\xa2\x66\xea\x5d\xa3\xf3\x66\x26\xf1\xc9\x14\x09\x59\xf9\x3c\x82\x57\xc8\xca\x5d\xf4\xc2\x18\xd0\x0a\xc1\x6f\x31\x48\x51\x28\x59\x18\x20\xf5\xab\x99\xdc\xaa\xf4\x79\x44\xfe\x51\xe9\x2e\x1e\x5f\xb8\x5c\x63\x0e\xb7\x2a\x7d\xd7\x68\xf9\x9a\x53\x2a\xc9\x49\x0d\xac\xed\xe7\x32\x2f\xef\x48\xf3\xa2\x40\xbd\xf0\x14\xb6\x08\xa1\xb4\xe5\x58\x5c\x9a\x49\x70\xd5\x9b\x14\x91\xfa\x5b\xdd\x01\xad\xb8\x89\x36\xd8\x1d\x33\x10\x16\xc3\xc8\x75\xaa\x2d\xc1\xd7\x49\x26\x54\xb6\x76\xd5\x6b\x65\x4b\xe6\xcb\x18\xfe\x70\xf6\x6e\x5e\x31\x72\x51\xa9\x9e\x42\xf4\xff\x81\x7a\x44\x71\x53\x62\x86\x36\x23\x4a\x15\xca\xdc\xc7\x10\x12\x6d\xa5\x0c\x9f\x99\x2a\x2b\x81\xe4\xdc\x00\xc9\x92\x71\xd1\x7c\x65\x4c\x66\x28\xdc\x8f\x0d\xe3\x1c\x97\xcc\x0a\xbf\x60\x67\xeb\xf5\xbc\xe1\x8f\xb2\xc9\xce\xb8\xe8\x6b\x47\xbe\xf8\xd3\x6a\xed\xca\xfa\x26\xda\x03\xcb\x91\x4b\xb8\x34\x15\xd7\xac\x3d\xa9\x0a\xc5\x84\x71\x1f\xc4\xcc\xda\x04\x47\xb9\x7f\x39\x0a\xfe\x03\x75\xed\xbe\x05\x32\xed\x9d\x37\xea\x98\x9e\xc5\xd7\x73\x4e\xd6\x90\xda\x76\xcf\xad\x89\x17\xea\xdc\xd3\x3a\xc1\xe5\xcc\x24\x37\xc9\xc2\xa7\x0e\xc2\xa1\xe3\xbe\xf0\xc0\x8e\xa0\xd2\xaa\xd0\x68\xcc\x9b\x51\x2e\x4f\xa2\x46\xad\x95\xde\xc6\x4c\x78\x4f\x63\xe7\x64\x5f\x3b\x4e\x6f\x37\x02\x25\x1a\xc3\x0a\x04\xbe\x8c\x62\xdb\x66\xec\x1c\x78\xfe\xff\xf4\xba\xf9\x5f\x4f\x7d\x74\xf7\x11\x27\x81\xbe\x66\x7e\x3b\x87\x9c\x9b\x4a\xb0\x1a\x0e\x6d\x95\x33\xc2\x1c\xd2\x3a\x00\x57\x1a\x18\x81\xcf\xb6\x26\x39\xc7\x8e\xbb\xe3\x0f\x1f\x67\xb1\x0a\xa9\xb2\xf0\x71\x7c\x26\xc5\x36\x29\xae\x98\x59\xc3\x4e\xbe\xad\x96\x5b\x01\xa2\xbb\xeb\x54\xe2\xda\xca\xd7\xa7\xcd\x65\x65\xa7\xef\x8f\xf3\xbe\x76\x44\xee\xab\xe6\x05\x97\x4c\xb4\xe7\x03\x78\xc3\x70\x18\x8e\x19\x3f\x71\xe6\x76\x18\xad\x0d\x4f\x02\x1d\x99\x13\xc1\xbd\x78\x1f\x6e\xff\x70\x18\xa9\x5e\x62\xa5\xf4\x5c\x98\xa1\x0a\x4e\x04\xf8\x57\x5f\x3b\x86\xf6\xa1\x83\xe6\x95\x66\xc2\x09\xb5\x78\x22\x9c\xab\xbe\x76\x0c\xe7\x63\x07\xe7\xfa\xc6\xa9\xcd\xc4\xa3\xd1\x58\x41\xd3\x11\x5d\x0e\xf5\x63\x4c\xbf\x0d\x30\x35\xca\x33\x91\x6d\x4e\xa9\x89\xd0\x3e\x6f\x4d\x88\xb1\x7d\xea\xb0\xb5\x7a\x2f\x42\xd7\x9d\x9b\x53\xd1\x7d\xd9\x9a\x10\xa3\x3b\x8e\x3c\xd7\x6a\x9e\x49\xd2\xf5\x4c\x78\x86\x98\xa6\x05\xf1\xb1\x7a\xe9\xa4\x86\x58\x59\x8d\xdf\xca\x34\xc1\x15\x7f\xf2\x6c\xf0\x0b\xc0\x88\xa9\x17\x5f\x9b\x51\xe6\x33\x80\x9f\xc9\x7c\x0a\x6c\x94\xf9\xcb\x40\x1f\x00\xdc\x84\x27\x05\xe1\x8b\x93\xeb\xec\x1b\x0e\xa1\xd1\xec\x7e\x47\xc8\x57\xcc\x7c\x95\x11\xb6\x52\xe5\x0d\xec\xb8\x4b\xdc\x0c\xbb\xde\x7d\xbc\x71\x5d\x2a\x8d\xbc\x90\xfd\xd1\x0e\x59\xf3\xe2\xe0\x41\xe3\xde\x17\x87\xfb\xc5\xe0\x65\x63\x11\xac\x0d\x2f\xdc\xd1\xeb\x45\x0c\xa7\xbb\xed\xdf\x6c\x05\xa9\x71\xd9\x58\x14\xce\xdd\x48\xd3\x82\x35\x9d\xf3\xe6\xde\xf3\xdd\xa2\xe6\x68\xe0\x8e\xd3\x2a\x5c\xfa\x61\xc9\x05\xa1\xde\xff\x86\x31\xc6\xc4\xf7\xc2\xfb\x78\x74\xcd\xf2\x0b\x59\x38\x3b\xbf\x8a\x43\x68\x1d\xfd\x2e\xdb\xc1\xa2\xd7\x5d\xf6\x76\xfc\x4c\x3a\xe1\x86\xe0\xe6\x00\x93\x4c\xd4\x86\x9b\xe7\x43\xdf\x24\xd2\x2e\xe4\xc3\x3c\x7a\x19\x6c\xbd\xe3\x11\x2e\xad\x43\x9a\x25\x83\x8d\xdb\x35\x4c\x0f\x51\x45\x31\xed\xce\x87\xc4\xa8\x25\x2d\x72\x74\x5d\xa4\xe9\x5a\x1c\xd7\x23\xea\x92\x1b\xd3\x4c\x75\xaa\xf0\x78\xf0\x78\xf0\x33\x00\x00\xff\xff\x3d\xaa\x76\x5c\x26\x15\x00\x00") func yaoModelsAgentExecutionModYaoBytes() ([]byte, error) { return bindataRead( @@ -3006,7 +3006,7 @@ func yaoModelsAgentExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/execution.mod.yao", size: 4953, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/execution.mod.yao", size: 5414, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3026,7 +3026,7 @@ func yaoModelsAgentMessageModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/message.mod.yao", size: 3712, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3046,7 +3046,7 @@ func yaoModelsAgentResumeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/resume.mod.yao", size: 3896, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3066,7 +3066,7 @@ func yaoModelsAgentSearchModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/agent/search.mod.yao", size: 3103, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3086,7 +3086,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3106,7 +3106,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3126,7 +3126,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3146,7 +3146,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3166,7 +3166,7 @@ func yaoModelsInvitationModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3186,7 +3186,7 @@ func yaoModelsJobCategoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3206,7 +3206,7 @@ func yaoModelsJobExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3226,7 +3226,7 @@ func yaoModelsJobJobModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6429, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6429, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3246,7 +3246,7 @@ func yaoModelsJobLogModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3266,7 +3266,7 @@ func yaoModelsKbCollectionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3286,7 +3286,7 @@ func yaoModelsKbDocumentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3306,7 +3306,7 @@ func yaoModelsMemberModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3326,7 +3326,7 @@ func yaoModelsRoleModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3346,7 +3346,7 @@ func yaoModelsTeamModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3366,7 +3366,7 @@ func yaoModelsUserOauth_accountModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3386,7 +3386,7 @@ func yaoModelsUserTypeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3406,7 +3406,7 @@ func yaoModelsUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1769220632, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3426,7 +3426,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3446,7 +3446,7 @@ func yaoStoresAgentCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3466,7 +3466,7 @@ func yaoStoresAgentMemoryChatXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/chat.xun.yao", size: 497, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/chat.xun.yao", size: 497, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3486,7 +3486,7 @@ func yaoStoresAgentMemoryContextXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/context.xun.yao", size: 507, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/context.xun.yao", size: 507, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3506,7 +3506,7 @@ func yaoStoresAgentMemoryTeamXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/team.xun.yao", size: 483, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/team.xun.yao", size: 483, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3526,7 +3526,7 @@ func yaoStoresAgentMemoryUserXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory/user.xun.yao", size: 489, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory/user.xun.yao", size: 489, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3546,7 +3546,7 @@ func yaoStoresCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3566,7 +3566,7 @@ func yaoStoresKbCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3586,7 +3586,7 @@ func yaoStoresKbStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/store.xun.yao", size: 373, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/kb/store.xun.yao", size: 373, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3606,7 +3606,7 @@ func yaoStoresOauthCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3626,7 +3626,7 @@ func yaoStoresOauthClientXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/client.xun.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/client.xun.yao", size: 377, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3646,7 +3646,7 @@ func yaoStoresOauthStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/store.xun.yao", size: 401, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/store.xun.yao", size: 401, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3666,7 +3666,7 @@ func yaoStoresStoreXunYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/store.xun.yao", size: 369, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/stores/store.xun.yao", size: 369, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3686,7 +3686,7 @@ func yaoUploadersAttachmentLocalYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1769080845, 0)} + info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1769220631, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/openapi/agent/robot/DESIGN.md b/openapi/agent/robot/DESIGN.md index 63caebdc..9fcb8882 100644 --- a/openapi/agent/robot/DESIGN.md +++ b/openapi/agent/robot/DESIGN.md @@ -253,9 +253,13 @@ type ExecutionResponse struct { Status string `json:"status"` Phase string `json:"phase"` Error *string `json:"error,omitempty"` - JobID string `json:"job_id"` - Name string `json:"name,omitempty"` // Localized execution name - CurrentTaskName string `json:"current_task_name,omitempty"` // Localized current task + + // UI display fields (from backend Execution) + // These are updated by executor at each phase for frontend display + Name string `json:"name,omitempty"` // Execution title + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description + + // Phase outputs (for detail view) Goals *GoalsResponse `json:"goals,omitempty"` Tasks []TaskResponse `json:"tasks,omitempty"` Current *CurrentState `json:"current,omitempty"` @@ -263,6 +267,17 @@ type ExecutionResponse struct { } ``` +**UI Display Fields Update Timeline:** + +| Phase | `Name` | `CurrentTaskName` | +|-------|--------|-------------------| +| Created | Human: from `input.messages[0]`
Clock/Event: "Preparing..." | "Starting..." | +| `inspiration` | - | "Analyzing context..." | +| `goals` complete | Extracted from first goal in `goals.content` | "Planning goals..." | +| `tasks` | - | "Breaking down tasks..." | +| `run` (each task) | - | Current task description | +| Completed/Failed | - | "Completed" / "Failed: {error}" | + ### 4.4 ResultResponse ```go @@ -491,13 +506,50 @@ data: {"error": "Something went wrong", "phase": "run"} ### 8.1 Locale Detection -Priority order: -1. Query parameter: `?locale=zh-CN` -2. Request body field: `locale: "zh-CN"` -3. Accept-Language header -4. Default: `en-US` +**For API requests (Human trigger):** -### 8.2 Localized Fields +Priority order: +1. Request body field: `locale: "zh-CN"` (in TriggerRequest) +2. Query parameter: `?locale=zh-CN` +3. Accept-Language header +4. Robot's `default_locale` config +5. System default: `en-US` + +**For Clock/Event triggers (no user context):** + +Priority order: +1. Robot's `default_locale` config (from `robot_config.default_locale`) +2. System default: `en-US` + +### 8.2 Robot Default Locale + +Robots can configure a default language for clock/event triggered executions: + +```go +// In RobotConfig (robot_config field in __yao.member) +type Config struct { + // ... other fields ... + DefaultLocale string `json:"default_locale,omitempty"` // "en-US", "zh-CN" +} +``` + +**Language resolution:** +```go +func getLocale(robot *Robot, input *TriggerInput) string { + // 1. Human trigger with explicit locale + if input != nil && input.Locale != "" { + return input.Locale + } + // 2. Robot configured default + if robot.Config != nil && robot.Config.DefaultLocale != "" { + return robot.Config.DefaultLocale + } + // 3. System default + return "en-US" +} +``` + +### 8.3 Localized Fields | Response Type | Localized Fields | |---------------|------------------| diff --git a/openapi/agent/robot/TODO.md b/openapi/agent/robot/TODO.md index c685f0a5..ae17bcbf 100644 --- a/openapi/agent/robot/TODO.md +++ b/openapi/agent/robot/TODO.md @@ -6,6 +6,188 @@ --- +## Field Alignment Review Summary + +> Last reviewed: 2026-01-23 + +### Robot Fields ✅ Fully Aligned + +| Backend (`types.go`) | Frontend (`types.ts`) | Status | +|---------------------|----------------------|--------| +| `member_id` | `member_id` | ✅ | +| `team_id` | `team_id` | ✅ | +| `display_name` | `display_name` | ✅ | +| `bio` | `bio` / `description` | ✅ | +| `name` (← member_id) | `name` | ✅ | +| `description` (← bio) | `description` | ✅ | +| `robot_status` | `robot_status` | ✅ | +| `autonomous_mode` | `autonomous_mode` | ✅ | +| `robot_config` | `robot_config` | ✅ | +| `robot_email` | `robot_email` | ✅ | +| All other fields | Same | ✅ | + +### Execution Fields ✅ Aligned + +| Backend (`types.go`) | Frontend (`types.ts`) | Status | +|---------------------|----------------------|--------| +| `id` | `id` | ✅ Aligned | +| `member_id` | `member_id` | ✅ | +| `team_id` | `team_id` | ✅ | +| `trigger_type` | `trigger_type` | ✅ | +| `status` | `status` | ✅ | +| `phase` | `phase` | ✅ | +| `start_time` | `start_time` | ✅ | +| `end_time` | `end_time` | ✅ | +| `error` | `error` | ✅ | +| `input` | `input` | ✅ Optional | +| Phase outputs | Same | ✅ Detail view | +| `name` | `name` | ✅ Added | +| `current_task_name` | `current_task_name` | ✅ Added | +| - | `job_id` | 🗑️ **Dead field, to be removed** | + +**Action Items:** +- [x] **Backend**: `Execution` struct - add `Name`, `CurrentTaskName` fields (see Improvement Plan below) +- [x] **Backend**: `RobotConfig` struct - add `DefaultLocale` field (see Improvement Plan below) +- [x] **Backend**: `TriggerInput` struct - add `Locale` field (see Improvement Plan below) +- [x] **Backend**: Database model `execution.mod.yao` - add `name`, `current_task_name` columns +- [x] **Backend**: Executor - update `Name`, `CurrentTaskName` at each phase +- [x] **Backend**: Store layer - add `UpdateUIFields()` method +- [x] **Backend**: Unit tests for UI fields and i18n (executor/standard/ui_fields_test.go, store/execution_test.go) +- [ ] **Frontend**: Remove `job_id` field from `types.ts` +- [ ] **Frontend**: Remove `job_id` mock data from `mock/data.ts` +- [ ] **Frontend**: Use `name` and `current_task_name` directly from API response + +--- + +### Improvement Plan: Execution UI Display Fields ✅ Implemented + +> **Problem:** Frontend needs to display "execution title" and "current task", which must be dynamically updated at different phases +> **Solution:** Backend manages these fields centrally; `Execution` struct gets new fields, executor updates them at each phase + +**1. Execution struct fields (`agent/robot/types/robot.go`):** ✅ +```go +type Execution struct { + // ... existing fields ... + + // UI display fields (updated by executor at each phase) + Name string `json:"name,omitempty"` // Execution title + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description +} +``` + +**2. Update timeline:** ✅ + +| Phase | `Name` | `CurrentTaskName` | +|-------|--------|-------------------| +| Created | Human: extract from `input.messages[0]`
Clock/Event: "Preparing..." (localized) | "Starting..." (localized) | +| `inspiration` | - | "Analyzing context..." (localized) | +| `goals` complete | Extract first line from `goals.content` | "Planning goals..." (localized) | +| `tasks` | - | "Breaking down tasks..." (localized) | +| `run` (each task) | - | Current `task` description (e.g., "Task 1/3: ...") | +| Completed/Failed | - | "Completed" / "Failed: {error}" (localized) | + +**3. Implementation files:** +- `agent/robot/types/robot.go` - Execution struct fields ✅ +- `agent/robot/store/execution.go` - UpdateUIFields() method ✅ +- `agent/robot/executor/standard/executor.go` - initUIFields(), updateUIFields(), i18n messages ✅ +- `agent/robot/executor/standard/inspiration.go` - Update CurrentTaskName ✅ +- `agent/robot/executor/standard/goals.go` - Update Name and CurrentTaskName ✅ +- `agent/robot/executor/standard/tasks.go` - Update CurrentTaskName ✅ +- `agent/robot/executor/standard/run.go` - Update CurrentTaskName for each task ✅ +- `yao/models/agent/execution.mod.yao` - Database columns ✅ + +--- + +### Improvement Plan: i18n Default Locale ✅ Implemented + +> **Problem:** Clock/Event triggers have no user context, unknown which language to use for generated content +> **Solution:** `RobotConfig` gets a default locale configuration field + +**1. RobotConfig struct field (`agent/robot/types/config.go`):** ✅ +```go +type Config struct { + // ... existing fields ... + DefaultLocale string `json:"default_locale,omitempty"` // "en" | "zh", default "en" +} + +// GetDefaultLocale returns the default locale (default: "en") +func (c *Config) GetDefaultLocale() string { + if c == nil || c.DefaultLocale == "" { + return "en" + } + return c.DefaultLocale +} +``` + +**2. TriggerInput struct field (`agent/robot/types/robot.go`):** ✅ +```go +type TriggerInput struct { + // ... existing fields ... + Locale string `json:"locale,omitempty"` // Language from human trigger +} +``` + +**3. Locale determination logic (`agent/robot/executor/standard/executor.go`):** ✅ +```go +func getEffectiveLocale(robot *Robot, input *TriggerInput) string { + // 1. Human trigger: use locale from request + if input != nil && input.Locale != "" { + return input.Locale + } + // 2. Clock/Event trigger: use Robot config + if robot != nil && robot.Config != nil { + return robot.Config.GetDefaultLocale() + } + // 3. System default + return "en" +} +``` + +**4. Locale source priority:** ✅ + +| Trigger Type | Locale Source | +|--------------|---------------| +| Human | Request `locale` → Robot `default_locale` → "en" | +| Event | Robot `default_locale` → "en" | +| Clock | Robot `default_locale` → "en" | + +**5. Localized messages (`executor.go`):** ✅ +```go +var uiMessages = map[string]map[string]string{ + "en": { + "preparing": "Preparing...", + "starting": "Starting...", + "scheduled_execution": "Scheduled execution", + "event_prefix": "Event: ", + "event_triggered": "Event triggered", + "analyzing_context": "Analyzing context...", + "planning_goals": "Planning goals...", + "breaking_down_tasks": "Breaking down tasks...", + "completed": "Completed", + "failed_prefix": "Failed: ", + "task_prefix": "Task", + }, + "zh": { + "preparing": "准备中...", + "starting": "启动中...", + "scheduled_execution": "定时执行", + // ... more Chinese messages + }, +} +``` + +> **Note:** User preference locale fallback deferred to future version + +### Deferred Features (Phase 5/6) + +| Feature | Current Status | Future Plan | +|---------|---------------|-------------| +| Trigger/Intervene UI | Backend done, frontend deferred | Phase 5 (requires SSE) | +| Real-time refresh | Polling 60s | Phase 6 (SSE streams) | +| Multi-turn chat | Not started | Phase 5 | + +--- + ## Implementation Strategy > **Integrate frontend immediately after each phase to validate deliverables.** @@ -42,8 +224,8 @@ └─ Locale parameter support 🟡 Medium Risk (Deferred): - Phase 5: Multi-turn Chat API - Phase 6: Real-time SSE Streams + Phase 5: Multi-turn Chat API + Trigger/Intervene UI + Phase 6: Real-time SSE Streams (replace polling) ``` --- @@ -277,88 +459,207 @@ --- -## 🟢 Phase 2: Execution Management ⬜ [Low Risk] +## 🟢 Phase 2: Execution Management [Backend ✅ | Frontend ⬜] + +> **Backend:** Steps 1-4 ✅ Complete (including UI fields and i18n) +> **Frontend:** Step 5 ⬜ Pending +> **Deferred:** Trigger/Intervene UI → Phase 5 (requires SSE) **Goal:** Execution listing, details, control, and trigger/intervene (single-submit mode) -**Risk:** 🟢 Low - Wraps existing API functions +**Risk:** 🟢 Low - Wraps existing `robot/api` functions +**Workflow:** 1. Implement All Endpoints → 2. Linter Check → 3. Code Review → 4. Unit Tests → 5. Frontend Integration -### 2.1 List Executions ⬜ +--- -- [ ] `execution.go` - GET /v1/robots/:id/executions -- [ ] Parse query params: `status`, `trigger_type`, `keyword`, `page`, `pagesize` -- [ ] Call `robot/api.GetExecutions()` -- [ ] Add derived fields: `name`, `current_task_name` -- [ ] Format response -- [ ] Test: `tests/robot/execution_list_test.go` +### Step 1: Implement All OpenAPI Endpoints ✅ -### 2.2 Get Execution ⬜ +> Location: `yao/openapi/agent/robot/` +> Calls: `yao/agent/robot/api/` (existing functions) -- [ ] GET /v1/robots/:id/executions/:exec_id -- [ ] Call `robot/api.GetExecution()` -- [ ] Full task details with localization -- [ ] Test: `tests/robot/execution_get_test.go` +#### 2.1.1 Types (`types.go`) ✅ -### 2.3 Execution Control ⬜ +- [x] `ExecutionFilter` - query params for listing +- [x] `ExecutionResponse` - single execution response +- [x] `ExecutionListResponse` - paginated list response +- [x] `ExecutionControlResponse` - pause/resume/cancel response +- [x] `TriggerRequest` - trigger execution request +- [x] `TriggerResponse` - trigger result response +- [x] `InterveneRequest` - human intervention request +- [x] `InterveneResponse` - intervention result response -- [ ] POST /v1/robots/:id/executions/:exec_id/pause - - [ ] Call `robot/api.Pause()` -- [ ] POST /v1/robots/:id/executions/:exec_id/resume - - [ ] Call `robot/api.Resume()` -- [ ] POST /v1/robots/:id/executions/:exec_id/cancel - - [ ] Call `robot/api.Stop()` -- [ ] POST /v1/robots/:id/executions/:exec_id/retry - - [ ] Re-trigger with same input -- [ ] Test: `tests/robot/execution_control_test.go` +#### 2.1.2 Execution Handlers (`execution.go`) ✅ -### 2.4 Execution Types ⬜ +> **Permission Note:** Execution permissions are inherited from the parent robot. +> Check robot's `__yao_team_id` and `__yao_created_by` for access control. -- [ ] Add to `types.go`: - - [ ] `ExecutionResponse` struct - - [ ] `TaskResponse` struct - - [ ] `CurrentStateResponse` struct - - [ ] `GoalsResponse` struct - - [ ] `DeliveryResultResponse` struct +- [x] `ListExecutions` - GET /v1/agent/robots/:id/executions + - Parse query: `status`, `trigger_type`, `keyword`, `page`, `pagesize` + - Call `robot/api.ListExecutions()` + - Permission: Check robot CanRead (via robot ID) +- [x] `GetExecution` - GET /v1/agent/robots/:id/executions/:exec_id + - Call `robot/api.GetExecution()` + - Permission: Check robot CanRead (via robot ID) +- [x] `PauseExecution` - POST /v1/agent/robots/:id/executions/:exec_id/pause + - Call `robot/api.PauseExecution()` + - Permission: Check robot CanWrite (via robot ID) +- [x] `ResumeExecution` - POST /v1/agent/robots/:id/executions/:exec_id/resume + - Call `robot/api.ResumeExecution()` + - Permission: Check robot CanWrite (via robot ID) +- [x] `CancelExecution` - POST /v1/agent/robots/:id/executions/:exec_id/cancel + - Call `robot/api.StopExecution()` + - Permission: Check robot CanWrite (via robot ID) -### 2.5 Trigger & Intervene (Single-Submit Mode) ⬜ +#### 2.1.3 Trigger Handlers (`trigger.go`) ✅ -> **Note:** This is single-submit mode. Multi-turn chat is deferred to Phase 5. +> **Permission Note:** Same as execution - check robot's permission. -- [ ] `trigger.go` - POST /v1/robots/:id/trigger -- [ ] Parse `TriggerRequest` (messages, attachments) -- [ ] Call `robot/api.Trigger()` -- [ ] Return execution ID and status -- [ ] Optional: Return SSE stream for progress -- [ ] Test: `tests/robot/trigger_test.go` +- [x] `TriggerRobot` - POST /v1/agent/robots/:id/trigger + - Parse `TriggerRequest` (messages, trigger_type) + - Call `robot/api.Trigger()` + - Return execution ID and status + - Permission: Check robot CanWrite (via robot ID) +- [x] `InterveneRobot` - POST /v1/agent/robots/:id/intervene + - Parse `InterveneRequest` (action, messages) + - Call `robot/api.Intervene()` + - Return result + - Permission: Check robot CanWrite (via robot ID) -- [ ] POST /v1/robots/:id/intervene -- [ ] Parse `InterveneRequest` -- [ ] Call `robot/api.Intervene()` -- [ ] Return result -- [ ] Test: `tests/robot/intervene_test.go` +#### 2.1.4 Route Registration (`robot.go`) ✅ -### 2.6 Trigger Types ⬜ +- [x] Add execution routes to `Attach()`: + - `GET /:id/executions` + - `GET /:id/executions/:exec_id` + - `POST /:id/executions/:exec_id/pause` + - `POST /:id/executions/:exec_id/resume` + - `POST /:id/executions/:exec_id/cancel` + - `POST /:id/trigger` + - `POST /:id/intervene` -- [ ] Add to `types.go`: - - [ ] `TriggerRequest` struct - - [ ] `TriggerResponse` struct - - [ ] `InterveneRequest` struct - - [ ] `InterveneResponse` struct - - [ ] `Message` struct - - [ ] `Attachment` struct +--- -### 2.7 Frontend Integration ⬜ +### Step 2: Linter Check ✅ -> Integrate immediately after backend completion +- [x] Run `ReadLints` on all modified files +- [x] Fix any linter errors +- [x] Verify imports are correct +- [x] Build verification passed -- [ ] SDK: Add execution methods to `robot.ts` - - [ ] `listExecutions(robotId, params)` - - [ ] `getExecution(robotId, execId)` - - [ ] `pauseExecution()`, `resumeExecution()`, `cancelExecution()` - - [ ] `triggerRobot(robotId, data)` - - [ ] `intervene(robotId, data)` -- [ ] Page: Execution list/detail page integration -- [ ] Page: Assign Task (trigger execution) integration -- [ ] Verify: E2E testing +--- + +### Step 3: Code Review ✅ + +- [x] Review type definitions (`types.go`) + - `ExecutionFilter`, `ExecutionResponse`, `ExecutionListResponse`, `ExecutionControlResponse` + - `TriggerRequest`, `TriggerResponse`, `InterveneRequest`, `InterveneResponse` + - Conversion functions: `NewExecutionListResponse`, `NewExecutionResponseFromExecution`, `NewExecutionResponseBrief` +- [x] Review permission handling + - All execution/trigger handlers check robot permission first + - Read permission for listing and getting executions + - Write permission for control (pause/resume/cancel), trigger, and intervene + - Permission inherited from parent robot (check via `YaoTeamID` and `YaoCreatedBy`) +- [x] Review error handling + - Fixed: Use `errors.Is()` instead of `==` for error comparison + - Proper HTTP status codes (400, 404, 403, 500) + - Consistent error response format +- [x] Review response formats + - Brief format for list view (omits phase outputs) + - Full format for detail view (includes all fields) + - Consistent with existing robot responses + +--- + +### Step 4: Unit Tests ✅ + +> Location: `yao/openapi/tests/agent/` +> Uses `testing.Short()` to skip AI/manager-dependent tests + +- [x] Create `robot_execution_test.go` + - [x] `TestListExecutions` - list executions with pagination/filters + - [x] `TestGetExecution` - get execution details, not found cases + - [x] `TestExecutionControl` - pause/resume/cancel endpoints + - [x] `TestExecutionPermissions` - permission inheritance from robot +- [x] Create `robot_trigger_test.go` + - [x] `TestTriggerRobot` - trigger with messages, action, invalid body + - [x] `TestInterveneRobot` - intervene with action, missing action validation + - [x] `TestTriggerPermissions` - permission inheritance from robot +- [x] All tests use `testing.Short()` to skip AI-dependent tests +- [x] Tests compile successfully +- [x] All tests pass (with manager not started gracefully handled) + +--- + +### Step 5: Frontend Integration ⬜ + +> Location: `cui/packages/cui/openapi/agent/robot/` +> **Note:** Trigger/Intervene API deferred to Phase 5 (waiting for SSE support) +> **Note:** Use 1-minute polling for execution list refresh (will switch to SSE in Phase 6) + +#### 5.1 Prerequisites ✅ + +> **Dependency:** Backend improvement plans completed (see "Improvement Plan" sections above) + +**Backend (Completed):** +- [x] `Execution` struct - add `Name`, `CurrentTaskName` fields +- [x] `RobotConfig` struct - add `DefaultLocale` field +- [x] `TriggerInput` struct - add `Locale` field +- [x] Executor - update `Name`, `CurrentTaskName` at each phase +- [x] Store - add `UpdateUIFields()` method +- [x] Unit tests for UI fields and i18n + +**Frontend Cleanup (Pending):** +- [x] Components already use `exec.id` (no changes needed) +- [ ] Remove `job_id` field from `types.ts` +- [ ] Remove `job_id` from `mock/data.ts` +- [ ] Use `name`/`current_task_name` directly from API response + +#### 5.2 SDK Types (`types.ts`) ⬜ + +- [ ] `ExecutionFilter` interface +- [ ] `Execution` interface (align with backend `ExecutionResponse`) +- [ ] `ExecutionListResponse` interface +- [ ] `ExecutionControlResponse` interface + +**Deferred to Phase 5 (SSE):** +- [ ] ~~`TriggerRequest` / `TriggerResponse` interfaces~~ +- [ ] ~~`InterveneRequest` / `InterveneResponse` interfaces~~ + +#### 5.3 SDK Methods (`robots.ts`) ⬜ + +- [ ] `ListExecutions(robotId, filter)` +- [ ] `GetExecution(robotId, execId)` +- [ ] `PauseExecution(robotId, execId)` +- [ ] `ResumeExecution(robotId, execId)` +- [ ] `CancelExecution(robotId, execId)` + +**Deferred to Phase 5 (SSE):** +- [ ] ~~`Trigger(robotId, data)`~~ +- [ ] ~~`Intervene(robotId, data)`~~ + +#### 5.4 Page Integration ⬜ + +- [ ] ActiveTab: Replace mock with `ListExecutions()` API + - [ ] Filter: `status=running|pending` + - [ ] Polling: 1-minute interval (60000ms) - will switch to SSE in Phase 6 +- [ ] HistoryTab: Replace mock with `ListExecutions()` API + - [ ] Filter: `status` filter, `keyword` search + - [ ] Pagination: page/pagesize + - [ ] Polling: 1-minute interval for list refresh + +**Deferred to Phase 5 (SSE):** +- [ ] ~~Assign Task Modal: Call `Trigger()` API~~ +- [ ] ~~GuideExecution: Call `Intervene()` API~~ + +#### 5.5 Polling vs SSE Strategy + +**Current (Phase 2):** Polling +- Refresh execution list every 60 seconds +- Manual refresh button for immediate update +- Acceptable latency for status display + +**Future (Phase 6):** SSE Real-time +- `GET /robots/:id/executions/stream` - real-time execution updates +- `GET /robots/stream` - robot status changes +- Instant updates, no polling delay --- @@ -455,12 +756,13 @@ --- -## 🟡 Phase 5: Multi-turn Chat API ⬜ [Medium Risk - Deferred] +## 🟡 Phase 5: Multi-turn Chat API + Trigger/Intervene UI ⬜ [Medium Risk - Deferred] > **Frontend Fallback:** Single-submit mode (user input → immediate execution) > **Risk:** 🟡 Medium - New stateful component +> **Dependency:** Requires SSE infrastructure (partially) -**Goal:** Multi-turn conversation before execution +**Goal:** Multi-turn conversation before execution + Human trigger/intervene UI ### 5.1 Backend Prerequisites ⬜ @@ -486,14 +788,36 @@ - [ ] Use conversation history as execution input - [ ] Auto-cleanup conversation after execution starts +### 5.4 Frontend Trigger/Intervene Integration (Deferred from Phase 2) ⬜ + +> **Note:** These features require SSE for proper UX (streaming response) +> Currently backend `/trigger` and `/intervene` endpoints exist but return immediately +> Frontend needs streaming response to show assistant's reaction before confirming + +**SDK Types:** +- [ ] `TriggerRequest` / `TriggerResponse` interfaces +- [ ] `InterveneRequest` / `InterveneResponse` interfaces +- [ ] `ChatMessage` interface for multi-turn + +**SDK Methods:** +- [ ] `Trigger(robotId, data)` - with SSE support +- [ ] `Intervene(robotId, data)` - with SSE support +- [ ] `Chat(robotId, data)` - multi-turn conversation SSE + +**Page Integration:** +- [ ] AssignTaskDrawer: Multi-turn chat before trigger +- [ ] GuideExecutionDrawer: Multi-turn intervention +- [ ] Real-time streaming response display + --- ## 🟡 Phase 6: Real-time SSE Streams ⬜ [Medium Risk - Deferred] -> **Frontend Fallback:** Polling (GET /executions every 3-5 seconds) +> **Frontend Current:** Polling every 60 seconds (1 minute) +> **Frontend Future:** SSE streams for instant updates > **Risk:** 🟡 Medium - Requires modification of executor/manager -**Goal:** SSE streams for real-time status updates +**Goal:** SSE streams for real-time status updates, replacing polling ### 6.1 Backend Event System ⬜ @@ -632,9 +956,9 @@ yao/openapi/tests/robot/ | 1. Core CRUD | 🟢 | ✅ | ✅ | Robot CRUD endpoints | | 1-FE Frontend Integration | 🟢 | - | ✅ | SDK ✅, Page Integration ✅, UI/UX ✅ | | 1.5 Manager Lifecycle | 🟢 | ✅ | - | Auto-start, auto-reload, graceful shutdown | -| 2. Execution | 🟢 | ⬜ | ⬜ | Execution listing, control, trigger | +| 2. Execution | 🟢 | ✅ | ⬜ | Execution listing, control, trigger (backend complete with UI fields & i18n) | | 3. Results/Activities | 🟢 | ⬜ | ⬜ | Deliverables and activity feed | -| 4. i18n | 🟢 | ⬜ | ⬜ | Locale parameter support | +| 4. i18n | 🟢 | ✅ | ⬜ | Locale parameter support (backend executor i18n complete) | | 5. Chat API | 🟡 | ⬜ | ⬜ | Multi-turn conversation (Deferred) | | 6. SSE Streams | 🟡 | ⬜ | ⬜ | Real-time status updates (Deferred) | @@ -809,8 +1133,8 @@ Each phase independently deliverable: | Phase | Backend | Frontend | Verifiable Features | |-------|---------|----------|---------------------| | 1 | ✅ | ✅ | Robot CRUD basic management | -| 2 | ⬜ | ⬜ | Execution list/control/trigger | +| 2 | ✅ | ⬜ | Execution list/control/trigger (backend with UI fields & i18n) | | 3 | ⬜ | ⬜ | Results/Activities viewing | -| 4 | ⬜ | ⬜ | Multi-language support | +| 4 | ✅ | ⬜ | Multi-language support (backend executor i18n) | | 5 | ⬜ | ⬜ | Multi-turn chat UX (optional) | | 6 | ⬜ | ⬜ | Real-time push (optional) | diff --git a/openapi/agent/robot/execution.go b/openapi/agent/robot/execution.go new file mode 100644 index 00000000..4446dd77 --- /dev/null +++ b/openapi/agent/robot/execution.go @@ -0,0 +1,348 @@ +package robot + +import ( + "errors" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/kun/log" + robotapi "github.com/yaoapp/yao/agent/robot/api" + robottypes "github.com/yaoapp/yao/agent/robot/types" + "github.com/yaoapp/yao/openapi/oauth/authorized" + "github.com/yaoapp/yao/openapi/response" +) + +// ==================== Execution Handlers ==================== +// Permission Note: Execution permissions are inherited from the parent robot. +// Check robot's __yao_team_id and __yao_created_by for access control. + +// ListExecutions lists executions for a robot +// GET /v1/agent/robots/:id/executions +func ListExecutions(c *gin.Context) { + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get robot ID from URL parameter + robotID := c.Param("id") + if robotID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "robot id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Create robot context + ctx := &robottypes.Context{} + + // Check robot permission first (executions inherit robot permission) + robotResp, err := robotapi.GetRobotResponse(ctx, robotID) + if err != nil { + if errors.Is(err, robottypes.ErrRobotNotFound) { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Robot not found: " + robotID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Check read permission on robot + if !CanRead(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to access this robot's executions", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Parse query parameters + var filter ExecutionFilter + if err := c.ShouldBindQuery(&filter); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid query parameters: " + err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Apply defaults + if filter.Page <= 0 { + filter.Page = 1 + } + if filter.PageSize <= 0 { + filter.PageSize = 20 + } + if filter.PageSize > 100 { + filter.PageSize = 100 + } + + // Build API query + query := &robotapi.ExecutionQuery{ + Page: filter.Page, + PageSize: filter.PageSize, + } + if filter.Status != "" { + query.Status = robottypes.ExecStatus(filter.Status) + } + if filter.TriggerType != "" { + query.Trigger = robottypes.TriggerType(filter.TriggerType) + } + + // Call API layer + result, err := robotapi.ListExecutions(ctx, robotID, query) + if err != nil { + log.Error("Failed to list executions for robot %s: %v", robotID, err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to list executions: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Convert to response (brief format for list) + data := make([]*ExecutionResponse, 0, len(result.Data)) + for _, exec := range result.Data { + data = append(data, NewExecutionResponseBrief(exec)) + } + + resp := &ExecutionListResponse{ + Data: data, + Total: result.Total, + Page: result.Page, + PageSize: result.PageSize, + } + response.RespondWithSuccess(c, response.StatusOK, resp) +} + +// GetExecution gets a single execution by ID +// GET /v1/agent/robots/:id/executions/:exec_id +func GetExecution(c *gin.Context) { + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get robot ID and execution ID from URL parameters + robotID := c.Param("id") + execID := c.Param("exec_id") + + if robotID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "robot id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + if execID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "execution id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Create robot context + ctx := &robottypes.Context{} + + // Check robot permission first + robotResp, err := robotapi.GetRobotResponse(ctx, robotID) + if err != nil { + if errors.Is(err, robottypes.ErrRobotNotFound) { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Robot not found: " + robotID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Check read permission on robot + if !CanRead(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to access this robot's executions", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Get execution + exec, err := robotapi.GetExecution(ctx, execID) + if err != nil { + log.Error("Failed to get execution %s: %v", execID, err) + + if err.Error() == "execution not found: "+execID { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Execution not found: " + execID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get execution: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Verify execution belongs to this robot + if exec.MemberID != robotID { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Execution does not belong to this robot", + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + + // Convert to response (full format for detail) + resp := NewExecutionResponseFromExecution(exec) + response.RespondWithSuccess(c, response.StatusOK, resp) +} + +// PauseExecution pauses a running execution +// POST /v1/agent/robots/:id/executions/:exec_id/pause +func PauseExecution(c *gin.Context) { + handleExecutionControl(c, "pause") +} + +// ResumeExecution resumes a paused execution +// POST /v1/agent/robots/:id/executions/:exec_id/resume +func ResumeExecution(c *gin.Context) { + handleExecutionControl(c, "resume") +} + +// CancelExecution cancels/stops an execution +// POST /v1/agent/robots/:id/executions/:exec_id/cancel +func CancelExecution(c *gin.Context) { + handleExecutionControl(c, "cancel") +} + +// handleExecutionControl handles pause/resume/cancel operations +func handleExecutionControl(c *gin.Context, action string) { + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get robot ID and execution ID from URL parameters + robotID := c.Param("id") + execID := c.Param("exec_id") + + if robotID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "robot id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + if execID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "execution id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Create robot context + ctx := &robottypes.Context{} + + // Check robot permission first + robotResp, err := robotapi.GetRobotResponse(ctx, robotID) + if err != nil { + if errors.Is(err, robottypes.ErrRobotNotFound) { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Robot not found: " + robotID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Check write permission on robot (control operations require write) + if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to control this robot's executions", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Execute the control action + var controlErr error + switch action { + case "pause": + controlErr = robotapi.PauseExecution(ctx, execID) + case "resume": + controlErr = robotapi.ResumeExecution(ctx, execID) + case "cancel": + controlErr = robotapi.StopExecution(ctx, execID) + default: + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid action: " + action, + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + if controlErr != nil { + log.Error("Failed to %s execution %s: %v", action, execID, controlErr) + + // Check for common errors + errMsg := controlErr.Error() + if errMsg == "execution_id is required" || errMsg == "execution not found" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Execution not found: " + execID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to " + action + " execution: " + controlErr.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Return success + resp := &ExecutionControlResponse{ + ExecutionID: execID, + Action: action + "d", // paused, resumed, cancelled + Success: true, + Message: "Execution " + action + "d successfully", + } + response.RespondWithSuccess(c, response.StatusOK, resp) +} diff --git a/openapi/agent/robot/robot.go b/openapi/agent/robot/robot.go index a4cf498e..9ec692f0 100644 --- a/openapi/agent/robot/robot.go +++ b/openapi/agent/robot/robot.go @@ -22,4 +22,15 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) { // Robot Status group.GET("/:id/status", GetRobotStatus) // GET /robots/:id/status - Get robot runtime status + + // Execution Management + group.GET("/:id/executions", ListExecutions) // GET /robots/:id/executions - List robot executions + group.GET("/:id/executions/:exec_id", GetExecution) // GET /robots/:id/executions/:exec_id - Get execution details + group.POST("/:id/executions/:exec_id/pause", PauseExecution) // POST /robots/:id/executions/:exec_id/pause - Pause execution + group.POST("/:id/executions/:exec_id/resume", ResumeExecution) // POST /robots/:id/executions/:exec_id/resume - Resume execution + group.POST("/:id/executions/:exec_id/cancel", CancelExecution) // POST /robots/:id/executions/:exec_id/cancel - Cancel execution + + // Trigger & Intervene + group.POST("/:id/trigger", TriggerRobot) // POST /robots/:id/trigger - Trigger robot execution + group.POST("/:id/intervene", InterveneRobot) // POST /robots/:id/intervene - Human intervention } diff --git a/openapi/agent/robot/trigger.go b/openapi/agent/robot/trigger.go new file mode 100644 index 00000000..2abb56ac --- /dev/null +++ b/openapi/agent/robot/trigger.go @@ -0,0 +1,271 @@ +package robot + +import ( + "errors" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/kun/log" + agentcontext "github.com/yaoapp/yao/agent/context" + robotapi "github.com/yaoapp/yao/agent/robot/api" + robottypes "github.com/yaoapp/yao/agent/robot/types" + "github.com/yaoapp/yao/openapi/oauth/authorized" + "github.com/yaoapp/yao/openapi/response" +) + +// ==================== Trigger Handlers ==================== +// Permission Note: Same as execution - check robot's permission. + +// TriggerRobot triggers a robot execution +// POST /v1/agent/robots/:id/trigger +func TriggerRobot(c *gin.Context) { + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get robot ID from URL parameter + robotID := c.Param("id") + if robotID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "robot id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse request body + var req TriggerRequest + if err := c.ShouldBindJSON(&req); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid request body: " + err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Create robot context + ctx := &robottypes.Context{} + + // Check robot permission first + robotResp, err := robotapi.GetRobotResponse(ctx, robotID) + if err != nil { + if errors.Is(err, robottypes.ErrRobotNotFound) { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Robot not found: " + robotID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Check write permission on robot (trigger requires write permission) + if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to trigger this robot", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Build API trigger request + apiReq := buildAPITriggerRequest(&req) + + // Call API layer + result, err := robotapi.Trigger(ctx, robotID, apiReq) + if err != nil { + log.Error("Failed to trigger robot %s: %v", robotID, err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to trigger robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Convert to response + resp := &TriggerResponse{ + Accepted: result.Accepted, + ExecutionID: result.ExecutionID, + Queued: result.Queued, + Message: result.Message, + } + + if result.Accepted { + response.RespondWithSuccess(c, response.StatusOK, resp) + } else { + // Trigger was not accepted (e.g., queue full, robot paused) + response.RespondWithSuccess(c, response.StatusOK, resp) + } +} + +// InterveneRobot performs human intervention on a robot +// POST /v1/agent/robots/:id/intervene +func InterveneRobot(c *gin.Context) { + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get robot ID from URL parameter + robotID := c.Param("id") + if robotID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "robot id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse request body + var req InterveneRequest + if err := c.ShouldBindJSON(&req); err != nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Invalid request body: " + err.Error(), + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Validate action + if req.Action == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "action is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Create robot context + ctx := &robottypes.Context{} + + // Check robot permission first + robotResp, err := robotapi.GetRobotResponse(ctx, robotID) + if err != nil { + if errors.Is(err, robottypes.ErrRobotNotFound) { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Robot not found: " + robotID, + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to get robot: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Check write permission on robot (intervention requires write permission) + if !CanWrite(c, authInfo, robotResp.YaoTeamID, robotResp.YaoCreatedBy) { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to intervene with this robot", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Build API trigger request for intervention + apiReq := &robotapi.TriggerRequest{ + Type: robottypes.TriggerHuman, + Action: robottypes.InterventionAction(req.Action), + PlanAt: req.PlanAt, + } + + // Convert messages + if len(req.Messages) > 0 { + apiReq.Messages = convertMessagesToContext(req.Messages) + } + + // Call API layer (Intervene uses TriggerHuman internally) + result, err := robotapi.Intervene(ctx, robotID, apiReq) + if err != nil { + log.Error("Failed to intervene with robot %s: %v", robotID, err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to intervene: " + err.Error(), + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Convert to response + resp := &InterveneResponse{ + Accepted: result.Accepted, + ExecutionID: result.ExecutionID, + Message: result.Message, + } + + response.RespondWithSuccess(c, response.StatusOK, resp) +} + +// ==================== Helper Functions ==================== + +// buildAPITriggerRequest builds robotapi.TriggerRequest from HTTP request +func buildAPITriggerRequest(req *TriggerRequest) *robotapi.TriggerRequest { + apiReq := &robotapi.TriggerRequest{} + + // Set trigger type (default to human) + switch req.TriggerType { + case "event": + apiReq.Type = robottypes.TriggerEvent + case "clock": + apiReq.Type = robottypes.TriggerClock + default: + apiReq.Type = robottypes.TriggerHuman + } + + // Human intervention fields + if req.Action != "" { + apiReq.Action = robottypes.InterventionAction(req.Action) + } + if len(req.Messages) > 0 { + apiReq.Messages = convertMessagesToContext(req.Messages) + } + + // Event fields + if req.Source != "" { + apiReq.Source = robottypes.EventSource(req.Source) + } + if req.EventType != "" { + apiReq.EventType = req.EventType + } + if req.Data != nil { + apiReq.Data = req.Data + } + + // Executor mode + if req.ExecutorMode != "" { + apiReq.ExecutorMode = robottypes.ExecutorMode(req.ExecutorMode) + } + + // i18n locale + if req.Locale != "" { + apiReq.Locale = req.Locale + } + + return apiReq +} + +// convertMessagesToContext converts MessageItem slice to agent context messages +func convertMessagesToContext(msgs []MessageItem) []agentcontext.Message { + result := make([]agentcontext.Message, 0, len(msgs)) + for _, m := range msgs { + result = append(result, agentcontext.Message{ + Role: agentcontext.MessageRole(m.Role), + Content: m.Content, + }) + } + return result +} diff --git a/openapi/agent/robot/types.go b/openapi/agent/robot/types.go index 7dd328de..2177701b 100644 --- a/openapi/agent/robot/types.go +++ b/openapi/agent/robot/types.go @@ -4,6 +4,7 @@ import ( "time" robotapi "github.com/yaoapp/yao/agent/robot/api" + robottypes "github.com/yaoapp/yao/agent/robot/types" ) // ==================== Request Types ==================== @@ -253,3 +254,185 @@ func NewStatusResponse(s *robotapi.RobotState) *StatusResponse { RunningIDs: s.RunningIDs, } } + +// ==================== Execution Types ==================== + +// ExecutionFilter - query params for listing executions +type ExecutionFilter struct { + Status string `form:"status"` // pending | running | paused | completed | failed | cancelled + TriggerType string `form:"trigger_type"` // clock | human | event + Keyword string `form:"keyword"` // search in execution details + Page int `form:"page"` + PageSize int `form:"pagesize"` +} + +// ExecutionResponse - single execution response +type ExecutionResponse struct { + ID string `json:"id"` + MemberID string `json:"member_id"` + TeamID string `json:"team_id"` + TriggerType string `json:"trigger_type"` + Status string `json:"status"` + Phase string `json:"phase"` + StartTime time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time,omitempty"` + Error string `json:"error,omitempty"` + + // UI display fields (updated by executor at each phase) + Name string `json:"name,omitempty"` // Execution title + CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description + + // Phase outputs (optional, included in detail view) + Inspiration interface{} `json:"inspiration,omitempty"` + Goals interface{} `json:"goals,omitempty"` + Tasks interface{} `json:"tasks,omitempty"` + Current interface{} `json:"current,omitempty"` + Results interface{} `json:"results,omitempty"` + Delivery interface{} `json:"delivery,omitempty"` + + // Input (optional, included in detail view) + Input interface{} `json:"input,omitempty"` +} + +// ExecutionListResponse - paginated list response +type ExecutionListResponse struct { + Data []*ExecutionResponse `json:"data"` + Total int `json:"total"` + Page int `json:"page"` + PageSize int `json:"pagesize"` +} + +// ExecutionControlResponse - response for pause/resume/cancel +type ExecutionControlResponse struct { + ExecutionID string `json:"execution_id"` + Action string `json:"action"` // paused | resumed | cancelled + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +// ==================== Trigger Types ==================== + +// TriggerRequest - HTTP request to trigger robot execution +type TriggerRequest struct { + // Trigger type: human | event | clock (defaults to human) + TriggerType string `json:"trigger_type,omitempty"` + + // Human intervention fields + Action string `json:"action,omitempty"` // task.add, goal.adjust, etc. + Messages []MessageItem `json:"messages,omitempty"` // user's input + + // Event fields + Source string `json:"source,omitempty"` // webhook | database + EventType string `json:"event_type,omitempty"` // lead.created, etc. + Data map[string]interface{} `json:"data,omitempty"` // event payload + + // Executor mode (optional) + ExecutorMode string `json:"executor_mode,omitempty"` // standard | fast | careful + + // i18n support + Locale string `json:"locale,omitempty"` // Locale for UI messages (e.g., "en", "zh") +} + +// MessageItem - a single message in trigger request +type MessageItem struct { + Role string `json:"role"` // user | assistant | system + Content string `json:"content"` // message text + Name string `json:"name,omitempty"` // optional name + FileID string `json:"file_id,omitempty"` // optional attachment +} + +// TriggerResponse - response after triggering +type TriggerResponse struct { + Accepted bool `json:"accepted"` + ExecutionID string `json:"execution_id,omitempty"` + Queued bool `json:"queued,omitempty"` + Message string `json:"message,omitempty"` +} + +// InterveneRequest - HTTP request for human intervention +type InterveneRequest struct { + Action string `json:"action"` // task.add, goal.adjust, etc. + Messages []MessageItem `json:"messages,omitempty"` // user's input + PlanAt *time.Time `json:"plan_at,omitempty"` // schedule for later +} + +// InterveneResponse - response after intervention +type InterveneResponse struct { + Accepted bool `json:"accepted"` + ExecutionID string `json:"execution_id,omitempty"` + Message string `json:"message,omitempty"` +} + +// ==================== Execution Conversion Functions ==================== + +// NewExecutionListResponse creates an ExecutionListResponse from api.ExecutionResult +func NewExecutionListResponse(e *robotapi.ExecutionResult) *ExecutionListResponse { + if e == nil { + return nil + } + + data := make([]*ExecutionResponse, 0, len(e.Data)) + for _, exec := range e.Data { + data = append(data, NewExecutionResponseFromExecution(exec)) + } + + return &ExecutionListResponse{ + Data: data, + Total: e.Total, + Page: e.Page, + PageSize: e.PageSize, + } +} + +// NewExecutionResponseFromExecution converts types.Execution to ExecutionResponse +func NewExecutionResponseFromExecution(exec *robottypes.Execution) *ExecutionResponse { + if exec == nil { + return nil + } + + return &ExecutionResponse{ + ID: exec.ID, + MemberID: exec.MemberID, + TeamID: exec.TeamID, + TriggerType: string(exec.TriggerType), + Status: string(exec.Status), + Phase: string(exec.Phase), + StartTime: exec.StartTime, + EndTime: exec.EndTime, + Error: exec.Error, + // UI display fields + Name: exec.Name, + CurrentTaskName: exec.CurrentTaskName, + // Phase outputs - include in detail view + Inspiration: exec.Inspiration, + Goals: exec.Goals, + Tasks: exec.Tasks, + Current: exec.Current, + Results: exec.Results, + Delivery: exec.Delivery, + Input: exec.Input, + } +} + +// NewExecutionResponseBrief creates a brief ExecutionResponse (for list view) +func NewExecutionResponseBrief(exec *robottypes.Execution) *ExecutionResponse { + if exec == nil { + return nil + } + + return &ExecutionResponse{ + ID: exec.ID, + MemberID: exec.MemberID, + TeamID: exec.TeamID, + TriggerType: string(exec.TriggerType), + Status: string(exec.Status), + Phase: string(exec.Phase), + StartTime: exec.StartTime, + EndTime: exec.EndTime, + Error: exec.Error, + // UI display fields - include in list view for display + Name: exec.Name, + CurrentTaskName: exec.CurrentTaskName, + // Omit phase outputs for list view + } +} diff --git a/openapi/tests/agent/robot_execution_test.go b/openapi/tests/agent/robot_execution_test.go new file mode 100644 index 00000000..cb304c86 --- /dev/null +++ b/openapi/tests/agent/robot_execution_test.go @@ -0,0 +1,416 @@ +package openapi_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/openapi" + "github.com/yaoapp/yao/openapi/tests/testutils" +) + +// TestListExecutions tests the execution listing endpoint +// GET /v1/agent/robots/:id/executions +func TestListExecutions(t *testing.T) { + if testing.Short() { + t.Skip("Skipping execution tests in short mode") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Execution List Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create a test robot + robotID := fmt.Sprintf("test_exec_list_%d", time.Now().UnixNano()) + createRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Execution List Robot") + defer deleteRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID) + + t.Run("ListExecutionsSuccess", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // Verify pagination fields exist + assert.Contains(t, response, "data") + assert.Contains(t, response, "page") + assert.Contains(t, response, "pagesize") + assert.Contains(t, response, "total") + + // Verify execution items structure (if any executions exist) + data, ok := response["data"].([]interface{}) + if ok && len(data) > 0 { + exec := data[0].(map[string]interface{}) + // Basic fields should exist + assert.Contains(t, exec, "id") + assert.Contains(t, exec, "status") + assert.Contains(t, exec, "phase") + // UI display fields (may be empty string, but field should be present in response) + // These are new fields added for frontend display + t.Logf("Execution response fields: id=%v, name=%v, current_task_name=%v", + exec["id"], exec["name"], exec["current_task_name"]) + } + }) + + t.Run("ListExecutionsWithPagination", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions?page=1&pagesize=5", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + assert.Equal(t, float64(1), response["page"]) + assert.Equal(t, float64(5), response["pagesize"]) + }) + + t.Run("ListExecutionsWithStatusFilter", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions?status=completed", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + assert.Contains(t, response, "data") + }) + + t.Run("ListExecutionsWithTriggerTypeFilter", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions?trigger_type=human", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + assert.Contains(t, response, "data") + }) + + t.Run("ListExecutionsRobotNotFound", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/executions", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("ListExecutionsUnauthorized", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions", nil) + require.NoError(t, err) + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +// TestGetExecution tests the execution detail endpoint +// GET /v1/agent/robots/:id/executions/:exec_id +func TestGetExecution(t *testing.T) { + if testing.Short() { + t.Skip("Skipping execution tests in short mode") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Execution Get Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create a test robot + robotID := fmt.Sprintf("test_exec_get_%d", time.Now().UnixNano()) + createRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Execution Get Robot") + defer deleteRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID) + + t.Run("GetExecutionNotFound", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions/non_existent_exec", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("GetExecutionRobotNotFound", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/executions/some_exec", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) +} + +// TestExecutionControl tests the execution control endpoints +// POST /v1/agent/robots/:id/executions/:exec_id/pause +// POST /v1/agent/robots/:id/executions/:exec_id/resume +// POST /v1/agent/robots/:id/executions/:exec_id/cancel +func TestExecutionControl(t *testing.T) { + if testing.Short() { + t.Skip("Skipping execution control tests in short mode") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Execution Control Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create a test robot + robotID := fmt.Sprintf("test_exec_ctrl_%d", time.Now().UnixNano()) + createRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Execution Control Robot") + defer deleteRobot(t, serverURL, baseURL, tokenInfo.AccessToken, robotID) + + t.Run("PauseExecutionNotFound", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/executions/non_existent_exec/pause", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // Should return error (404 or 500 depending on implementation) + assert.True(t, resp.StatusCode >= 400, "Expected error status code") + }) + + t.Run("ResumeExecutionNotFound", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/executions/non_existent_exec/resume", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.True(t, resp.StatusCode >= 400, "Expected error status code") + }) + + t.Run("CancelExecutionNotFound", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/executions/non_existent_exec/cancel", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.True(t, resp.StatusCode >= 400, "Expected error status code") + }) + + t.Run("PauseExecutionRobotNotFound", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/executions/some_exec/pause", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("ControlExecutionUnauthorized", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/executions/some_exec/pause", nil) + require.NoError(t, err) + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) +} + +// TestExecutionPermissions tests execution permission inheritance from robot +func TestExecutionPermissions(t *testing.T) { + if testing.Short() { + t.Skip("Skipping execution permission tests in short mode") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client + client := testutils.RegisterTestClient(t, "Execution Permission Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + + // Create User 1 + token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + user1ID := token1.UserID + + // Create User 2 + token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // User 1 creates a robot + robotID := fmt.Sprintf("test_exec_perm_%d", time.Now().UnixNano()) + createRobotWithTeam(t, serverURL, baseURL, token1.AccessToken, robotID, "Permission Test Robot", user1ID) + defer deleteRobot(t, serverURL, baseURL, token1.AccessToken, robotID) + + t.Run("OwnerCanListExecutions", func(t *testing.T) { + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Logf("Owner (User 1) successfully listed executions for their robot") + }) + + t.Run("OtherUserExecutionAccess", func(t *testing.T) { + // User 2 attempts to list executions for User 1's robot + // With system:root scope this might succeed + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/executions", nil) + require.NoError(t, err) + + req.Header.Set("Authorization", "Bearer "+token2.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + t.Logf("User 2 execution list attempt status: %d (with system:root scope)", resp.StatusCode) + }) +} + +// ==================== Helper Functions ==================== + +func createRobot(t *testing.T, serverURL, baseURL, token, robotID, displayName string) { + createData := map[string]interface{}{ + "member_id": robotID, + "team_id": "test_team_001", + "display_name": displayName, + } + + body, _ := json.Marshal(createData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() +} + +func createRobotWithTeam(t *testing.T, serverURL, baseURL, token, robotID, displayName, teamID string) { + createData := map[string]interface{}{ + "member_id": robotID, + "team_id": teamID, + "display_name": displayName, + } + + body, _ := json.Marshal(createData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() +} + +func deleteRobot(t *testing.T, serverURL, baseURL, token, robotID string) { + req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + if resp != nil { + resp.Body.Close() + } +} diff --git a/openapi/tests/agent/robot_test.go b/openapi/tests/agent/robot_test.go index 559c7252..92ad12aa 100644 --- a/openapi/tests/agent/robot_test.go +++ b/openapi/tests/agent/robot_test.go @@ -214,11 +214,11 @@ func TestCreateRobot(t *testing.T) { createdRobotIDs = append(createdRobotIDs, robotID) }) - t.Run("CreateRobotMissingRequiredFields", func(t *testing.T) { - // Missing member_id + t.Run("CreateRobotMissingDisplayName", func(t *testing.T) { + // Missing display_name (the only required field) createData := map[string]interface{}{ - "team_id": "test_team_001", - "display_name": "Test Robot", + "team_id": "test_team_001", + // display_name is missing } body, _ := json.Marshal(createData) @@ -235,6 +235,41 @@ func TestCreateRobot(t *testing.T) { assert.Equal(t, http.StatusBadRequest, resp.StatusCode) }) + t.Run("CreateRobotAutoGenerateMemberID", func(t *testing.T) { + // member_id is optional - should be auto-generated + createData := map[string]interface{}{ + "team_id": "test_team_001", + "display_name": "Auto ID Robot", + } + + body, _ := json.Marshal(createData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // Verify member_id was auto-generated + memberID, ok := response["member_id"].(string) + assert.True(t, ok, "member_id should be a string") + assert.NotEmpty(t, memberID, "member_id should be auto-generated") + assert.Len(t, memberID, 12, "auto-generated member_id should be 12 digits") + t.Logf("Auto-generated member_id: %s", memberID) + + // Cleanup + createdRobotIDs = append(createdRobotIDs, memberID) + }) + t.Run("CreateRobotDuplicate", func(t *testing.T) { robotID := fmt.Sprintf("test_robot_dup_%d", time.Now().UnixNano()) diff --git a/openapi/tests/agent/robot_trigger_test.go b/openapi/tests/agent/robot_trigger_test.go new file mode 100644 index 00000000..910ae833 --- /dev/null +++ b/openapi/tests/agent/robot_trigger_test.go @@ -0,0 +1,502 @@ +package openapi_test + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/yaoapp/yao/openapi" + "github.com/yaoapp/yao/openapi/tests/testutils" +) + +// TestTriggerRobot tests the robot trigger endpoint +// POST /v1/agent/robots/:id/trigger +func TestTriggerRobot(t *testing.T) { + if testing.Short() { + t.Skip("Skipping trigger tests in short mode (requires AI/manager)") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Trigger Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create a test robot + robotID := fmt.Sprintf("test_trigger_%d", time.Now().UnixNano()) + createRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Trigger Test Robot") + defer deleteRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID) + + t.Run("TriggerRobotBasic", func(t *testing.T) { + triggerData := map[string]interface{}{ + "trigger_type": "human", + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Hello, please help me with a task", + }, + }, + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // If manager is not started, we get a 500 error (expected in test environment) + // In production with manager running, response should contain "accepted" field + if resp.StatusCode == http.StatusInternalServerError { + // Expected when robot manager is not started + assert.Contains(t, response, "error_description") + t.Logf("Trigger response (manager not started): status=%d, error=%v", resp.StatusCode, response["error_description"]) + } else { + // Manager is running - verify accepted field + assert.Contains(t, response, "accepted") + t.Logf("Trigger response: status=%d, accepted=%v", resp.StatusCode, response["accepted"]) + } + }) + + t.Run("TriggerRobotWithAction", func(t *testing.T) { + triggerData := map[string]interface{}{ + "trigger_type": "human", + "action": "task.add", + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Add a new task: Review quarterly report", + }, + }, + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var response map[string]interface{} + json.NewDecoder(resp.Body).Decode(&response) + + // If manager is not started, we get a 500 error (expected in test environment) + if resp.StatusCode == http.StatusInternalServerError { + assert.Contains(t, response, "error_description") + t.Logf("Trigger with action response (manager not started): status=%d", resp.StatusCode) + } else { + assert.Contains(t, response, "accepted") + t.Logf("Trigger with action response: status=%d", resp.StatusCode) + } + }) + + t.Run("TriggerRobotWithLocale", func(t *testing.T) { + // Test the new locale parameter for i18n support + triggerData := map[string]interface{}{ + "trigger_type": "human", + "locale": "zh", // Chinese locale + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "请帮我分析销售数据", + }, + }, + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var response map[string]interface{} + json.NewDecoder(resp.Body).Decode(&response) + + // If manager is not started, we get a 500 error (expected in test environment) + if resp.StatusCode == http.StatusInternalServerError { + assert.Contains(t, response, "error_description") + t.Logf("Trigger with locale response (manager not started): status=%d", resp.StatusCode) + } else { + assert.Contains(t, response, "accepted") + t.Logf("Trigger with locale response: status=%d, accepted=%v", resp.StatusCode, response["accepted"]) + } + }) + + t.Run("TriggerRobotNotFound", func(t *testing.T) { + triggerData := map[string]interface{}{ + "trigger_type": "human", + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("TriggerRobotUnauthorized", func(t *testing.T) { + triggerData := map[string]interface{}{ + "trigger_type": "human", + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("TriggerRobotInvalidBody", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer([]byte("invalid json"))) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +// TestInterveneRobot tests the robot intervention endpoint +// POST /v1/agent/robots/:id/intervene +func TestInterveneRobot(t *testing.T) { + if testing.Short() { + t.Skip("Skipping intervene tests in short mode (requires AI/manager)") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Intervene Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create a test robot + robotID := fmt.Sprintf("test_intervene_%d", time.Now().UnixNano()) + createRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Intervene Test Robot") + defer deleteRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID) + + t.Run("InterveneRobotBasic", func(t *testing.T) { + interveneData := map[string]interface{}{ + "action": "task.add", + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Please add a high priority task", + }, + }, + } + + body, _ := json.Marshal(interveneData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + require.NotNil(t, resp) + defer resp.Body.Close() + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + require.NoError(t, err) + + // If manager is not started, we get a 500 error (expected in test environment) + if resp.StatusCode == http.StatusInternalServerError { + assert.Contains(t, response, "error_description") + t.Logf("Intervene response (manager not started): status=%d, error=%v", resp.StatusCode, response["error_description"]) + } else { + assert.Contains(t, response, "accepted") + t.Logf("Intervene response: status=%d, accepted=%v", resp.StatusCode, response["accepted"]) + } + }) + + t.Run("InterveneRobotMissingAction", func(t *testing.T) { + interveneData := map[string]interface{}{ + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Some message", + }, + }, + } + + body, _ := json.Marshal(interveneData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // Action is required + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("InterveneRobotNotFound", func(t *testing.T) { + interveneData := map[string]interface{}{ + "action": "task.add", + } + + body, _ := json.Marshal(interveneData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/intervene", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + }) + + t.Run("InterveneRobotUnauthorized", func(t *testing.T) { + interveneData := map[string]interface{}{ + "action": "task.add", + } + + body, _ := json.Marshal(interveneData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + }) + + t.Run("InterveneRobotInvalidBody", func(t *testing.T) { + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer([]byte("invalid json"))) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) +} + +// TestTriggerPermissions tests trigger permission inheritance from robot +func TestTriggerPermissions(t *testing.T) { + if testing.Short() { + t.Skip("Skipping trigger permission tests in short mode") + } + + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client + client := testutils.RegisterTestClient(t, "Trigger Permission Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + + // Create User 1 + token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + user1ID := token1.UserID + + // Create User 2 + token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // User 1 creates a robot + robotID := fmt.Sprintf("test_trig_perm_%d", time.Now().UnixNano()) + createRobotWithTeamForTrigger(t, serverURL, baseURL, token1.AccessToken, robotID, "Trigger Perm Robot", user1ID) + defer deleteRobotForTrigger(t, serverURL, baseURL, token1.AccessToken, robotID) + + t.Run("OwnerCanTrigger", func(t *testing.T) { + triggerData := map[string]interface{}{ + "trigger_type": "human", + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Owner triggering robot", + }, + }, + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // Owner should be able to trigger (may fail at manager level with 500, but not 403 permission denied) + // 500 = manager not started (acceptable), 403 = permission denied (not acceptable) + assert.NotEqual(t, http.StatusForbidden, resp.StatusCode, "Owner should have permission to trigger") + t.Logf("Owner trigger attempt status: %d", resp.StatusCode) + }) + + t.Run("OwnerCanIntervene", func(t *testing.T) { + interveneData := map[string]interface{}{ + "action": "task.add", + "messages": []map[string]interface{}{ + { + "role": "user", + "content": "Owner intervention", + }, + }, + } + + body, _ := json.Marshal(interveneData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + // 500 = manager not started (acceptable), 403 = permission denied (not acceptable) + assert.NotEqual(t, http.StatusForbidden, resp.StatusCode, "Owner should have permission to intervene") + t.Logf("Owner intervene attempt status: %d", resp.StatusCode) + }) + + t.Run("OtherUserTriggerAccess", func(t *testing.T) { + // User 2 attempts to trigger User 1's robot + // With system:root scope this might succeed + triggerData := map[string]interface{}{ + "trigger_type": "human", + } + + body, _ := json.Marshal(triggerData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token2.AccessToken) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + t.Logf("User 2 trigger attempt status: %d (with system:root scope)", resp.StatusCode) + }) +} + +// ==================== Helper Functions ==================== + +func createRobotForTrigger(t *testing.T, serverURL, baseURL, token, robotID, displayName string) { + createData := map[string]interface{}{ + "member_id": robotID, + "team_id": "test_team_001", + "display_name": displayName, + } + + body, _ := json.Marshal(createData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() +} + +func createRobotWithTeamForTrigger(t *testing.T, serverURL, baseURL, token, robotID, displayName, teamID string) { + createData := map[string]interface{}{ + "member_id": robotID, + "team_id": teamID, + "display_name": displayName, + } + + body, _ := json.Marshal(createData) + req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() +} + +func deleteRobotForTrigger(t *testing.T, serverURL, baseURL, token, robotID string) { + req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil) + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + if resp != nil { + resp.Body.Close() + } +} diff --git a/yao/models/agent/execution.mod.yao b/yao/models/agent/execution.mod.yao index 288b345d..7aedf00f 100644 --- a/yao/models/agent/execution.mod.yao +++ b/yao/models/agent/execution.mod.yao @@ -94,6 +94,22 @@ "comment": "Error message if execution failed", "nullable": true }, + { + "name": "name", + "type": "string", + "label": "Name", + "comment": "Execution title for UI display (updated by executor at goals phase)", + "length": 512, + "nullable": true + }, + { + "name": "current_task_name", + "type": "string", + "label": "Current Task Name", + "comment": "Current task description for UI display (updated by executor at run phase)", + "length": 512, + "nullable": true + }, { "name": "input", "type": "json",