Add Description Field to Task Struct and Enhance Execution Management
- Introduced a new `Description` field in the `Task` struct for a human-readable task description, improving UI clarity. - Updated the `ParseTask` function to save the description from input data and convert it to a message if no explicit messages are provided. - Enhanced the `Executor` to update UI fields with localized messages during task execution phases, ensuring better user feedback. - Implemented a new method in the `ExecutionStore` to persist task status updates, allowing real-time UI updates. - Added unit tests to validate the new task description handling and UI updates during execution phases.
This commit is contained in:
parent
de9e2c589b
commit
28d5730289
15 changed files with 651 additions and 221 deletions
|
|
@ -351,6 +351,7 @@ P2 Agent reads Goals markdown and breaks into executable tasks:
|
||||||
```go
|
```go
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string // unique task ID
|
ID string // unique task ID
|
||||||
|
Description string // human-readable task description (for UI display)
|
||||||
Messages []context.Message // original input (text, images, files, audio)
|
Messages []context.Message // original input (text, images, files, audio)
|
||||||
GoalRef string // reference to goal (e.g., "Goal 1")
|
GoalRef string // reference to goal (e.g., "Goal 1")
|
||||||
Source TaskSource // auto | human | event
|
Source TaskSource // auto | human | event
|
||||||
|
|
|
||||||
|
|
@ -1314,10 +1314,11 @@ type DeliveryTarget struct {
|
||||||
|
|
||||||
// Task - planned task (structured, for execution)
|
// Task - planned task (structured, for execution)
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Messages []context.Message `json:"messages"` // original input (text, images, files)
|
Description string `json:"description,omitempty"` // human-readable task description (for UI display)
|
||||||
GoalRef string `json:"goal_ref,omitempty"` // reference to goal (e.g., "Goal 1")
|
Messages []context.Message `json:"messages"` // original input (text, images, files)
|
||||||
Source TaskSource `json:"source"` // auto | human | event
|
GoalRef string `json:"goal_ref,omitempty"` // reference to goal (e.g., "Goal 1")
|
||||||
|
Source TaskSource `json:"source"` // auto | human | event
|
||||||
|
|
||||||
// Executor
|
// Executor
|
||||||
ExecutorType ExecutorType `json:"executor_type"`
|
ExecutorType ExecutorType `json:"executor_type"`
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ const memberModel = "__yao.member"
|
||||||
// robotStore is the shared robot store instance
|
// robotStore is the shared robot store instance
|
||||||
var robotStore = store.NewRobotStore()
|
var robotStore = store.NewRobotStore()
|
||||||
|
|
||||||
|
// executionStore is the shared execution store instance
|
||||||
|
var executionStore = store.NewExecutionStore()
|
||||||
|
|
||||||
// GetRobot returns a robot by member ID
|
// GetRobot returns a robot by member ID
|
||||||
// Returns the robot from cache if available, otherwise loads from database
|
// Returns the robot from cache if available, otherwise loads from database
|
||||||
func GetRobot(ctx *types.Context, memberID string) (*types.Robot, error) {
|
func GetRobot(ctx *types.Context, memberID string) (*types.Robot, error) {
|
||||||
|
|
@ -95,7 +98,6 @@ func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
|
||||||
DisplayName: robot.DisplayName,
|
DisplayName: robot.DisplayName,
|
||||||
Bio: robot.Bio,
|
Bio: robot.Bio,
|
||||||
Status: robot.Status,
|
Status: robot.Status,
|
||||||
Running: robot.RunningCount(),
|
|
||||||
MaxRunning: 2, // default
|
MaxRunning: 2, // default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,11 +111,33 @@ func GetRobotStatus(ctx *types.Context, memberID string) (*RobotState, error) {
|
||||||
state.MaxRunning = robot.Config.Quota.GetMax()
|
state.MaxRunning = robot.Config.Quota.GetMax()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get running execution IDs
|
// Get running execution IDs from ExecutionStore (more reliable than in-memory)
|
||||||
executions := robot.GetExecutions()
|
// This ensures we get accurate status even when robot is loaded from database
|
||||||
state.RunningIDs = make([]string, 0, len(executions))
|
runningExecs, err := executionStore.List(context.Background(), &store.ListOptions{
|
||||||
for _, exec := range executions {
|
MemberID: memberID,
|
||||||
state.RunningIDs = append(state.RunningIDs, exec.ID)
|
Status: types.ExecRunning,
|
||||||
|
Limit: 100,
|
||||||
|
})
|
||||||
|
if err == nil && len(runningExecs) > 0 {
|
||||||
|
state.Running = len(runningExecs)
|
||||||
|
state.RunningIDs = make([]string, 0, len(runningExecs))
|
||||||
|
for _, exec := range runningExecs {
|
||||||
|
state.RunningIDs = append(state.RunningIDs, exec.ExecutionID)
|
||||||
|
}
|
||||||
|
// Update status based on running count
|
||||||
|
state.Status = types.RobotWorking
|
||||||
|
} else {
|
||||||
|
// No running executions from store, check in-memory
|
||||||
|
executions := robot.GetExecutions()
|
||||||
|
state.Running = len(executions)
|
||||||
|
state.RunningIDs = make([]string, 0, len(executions))
|
||||||
|
for _, exec := range executions {
|
||||||
|
state.RunningIDs = append(state.RunningIDs, exec.ID)
|
||||||
|
}
|
||||||
|
// If there are running executions in memory, update status
|
||||||
|
if state.Running > 0 {
|
||||||
|
state.Status = types.RobotWorking
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set last run time
|
// Set last run time
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ func (e *Executor) RunDelivery(ctx *robottypes.Context, exec *robottypes.Executi
|
||||||
return fmt.Errorf("robot not found in 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, "generating_delivery"))
|
||||||
|
|
||||||
// Get agent ID for delivery phase
|
// Get agent ID for delivery phase
|
||||||
agentID := "__yao.delivery" // default
|
agentID := "__yao.delivery" // default
|
||||||
if robot.Config != nil && robot.Config.Resources != nil {
|
if robot.Config != nil && robot.Config.Resources != nil {
|
||||||
|
|
@ -108,6 +112,10 @@ func (e *Executor) routeToDeliveryCenter(ctx *robottypes.Context, exec *robottyp
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update UI field to show delivery is in progress
|
||||||
|
locale := getEffectiveLocale(robot, exec.Input)
|
||||||
|
e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "sending_delivery"))
|
||||||
|
|
||||||
// Create Delivery Center and execute
|
// Create Delivery Center and execute
|
||||||
center := NewDeliveryCenter()
|
center := NewDeliveryCenter()
|
||||||
results, err := center.Deliver(ctx, exec.Delivery.Content, &robottypes.DeliveryContext{
|
results, err := center.Deliver(ctx, exec.Delivery.Content, &robottypes.DeliveryContext{
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
type Executor struct {
|
type Executor struct {
|
||||||
config types.Config
|
config types.Config
|
||||||
store *store.ExecutionStore
|
store *store.ExecutionStore
|
||||||
|
robotStore *store.RobotStore
|
||||||
execCount atomic.Int32
|
execCount atomic.Int32
|
||||||
currentCount atomic.Int32
|
currentCount atomic.Int32
|
||||||
onStart func()
|
onStart func()
|
||||||
|
|
@ -29,15 +30,17 @@ type Executor struct {
|
||||||
// New creates a new standard executor
|
// New creates a new standard executor
|
||||||
func New() *Executor {
|
func New() *Executor {
|
||||||
return &Executor{
|
return &Executor{
|
||||||
store: store.NewExecutionStore(),
|
store: store.NewExecutionStore(),
|
||||||
|
robotStore: store.NewRobotStore(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithConfig creates a new standard executor with configuration
|
// NewWithConfig creates a new standard executor with configuration
|
||||||
func NewWithConfig(config types.Config) *Executor {
|
func NewWithConfig(config types.Config) *Executor {
|
||||||
return &Executor{
|
return &Executor{
|
||||||
config: config,
|
config: config,
|
||||||
store: store.NewExecutionStore(),
|
store: store.NewExecutionStore(),
|
||||||
|
robotStore: store.NewRobotStore(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +97,19 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
}).Warn("Execution quota exceeded")
|
}).Warn("Execution quota exceeded")
|
||||||
return nil, robottypes.ErrQuotaExceeded
|
return nil, robottypes.ErrQuotaExceeded
|
||||||
}
|
}
|
||||||
defer robot.RemoveExecution(exec.ID)
|
// Defer: remove execution from robot's tracking and update robot status if no more executions
|
||||||
|
defer func() {
|
||||||
|
robot.RemoveExecution(exec.ID)
|
||||||
|
// Update robot status to idle if no more running executions
|
||||||
|
if robot.RunningCount() == 0 && !e.config.SkipPersistence && e.robotStore != nil {
|
||||||
|
if err := e.robotStore.UpdateStatus(ctx.Context, robot.MemberID, robottypes.RobotIdle); err != nil {
|
||||||
|
log.With(log.F{
|
||||||
|
"member_id": robot.MemberID,
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to update robot status to idle: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Track execution count
|
// Track execution count
|
||||||
e.execCount.Add(1)
|
e.execCount.Add(1)
|
||||||
|
|
@ -127,6 +142,16 @@ func (e *Executor) Execute(ctx *robottypes.Context, robot *robottypes.Robot, tri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update robot status to working (when execution starts)
|
||||||
|
if !e.config.SkipPersistence && e.robotStore != nil {
|
||||||
|
if err := e.robotStore.UpdateStatus(ctx.Context, robot.MemberID, robottypes.RobotWorking); err != nil {
|
||||||
|
log.With(log.F{
|
||||||
|
"member_id": robot.MemberID,
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to update robot status to working: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for simulated failure (for testing)
|
// Check for simulated failure (for testing)
|
||||||
if dataStr, ok := data.(string); ok && dataStr == "simulate_failure" {
|
if dataStr, ok := data.(string); ok && dataStr == "simulate_failure" {
|
||||||
exec.Status = robottypes.ExecFailed
|
exec.Status = robottypes.ExecFailed
|
||||||
|
|
@ -211,6 +236,17 @@ func (e *Executor) runPhase(ctx *robottypes.Context, exec *robottypes.Execution,
|
||||||
"phase": string(phase),
|
"phase": string(phase),
|
||||||
}).Info("Phase started: %s", phase)
|
}).Info("Phase started: %s", phase)
|
||||||
|
|
||||||
|
// Persist phase change immediately (so frontend sees current phase)
|
||||||
|
if !e.config.SkipPersistence && e.store != nil {
|
||||||
|
if err := e.store.UpdatePhase(ctx.Context, exec.ID, phase, nil); err != nil {
|
||||||
|
log.With(log.F{
|
||||||
|
"execution_id": exec.ID,
|
||||||
|
"phase": string(phase),
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to persist phase start: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if e.config.OnPhaseStart != nil {
|
if e.config.OnPhaseStart != nil {
|
||||||
e.config.OnPhaseStart(phase)
|
e.config.OnPhaseStart(phase)
|
||||||
}
|
}
|
||||||
|
|
@ -381,6 +417,9 @@ var uiMessages = map[string]map[string]string{
|
||||||
"analyzing_context": "Analyzing context...",
|
"analyzing_context": "Analyzing context...",
|
||||||
"planning_goals": "Planning goals...",
|
"planning_goals": "Planning goals...",
|
||||||
"breaking_down_tasks": "Breaking down tasks...",
|
"breaking_down_tasks": "Breaking down tasks...",
|
||||||
|
"generating_delivery": "Generating delivery content...",
|
||||||
|
"sending_delivery": "Sending delivery...",
|
||||||
|
"learning_from_exec": "Learning from execution...",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"failed_prefix": "Failed at ",
|
"failed_prefix": "Failed at ",
|
||||||
"task_prefix": "Task",
|
"task_prefix": "Task",
|
||||||
|
|
@ -401,6 +440,9 @@ var uiMessages = map[string]map[string]string{
|
||||||
"analyzing_context": "分析上下文...",
|
"analyzing_context": "分析上下文...",
|
||||||
"planning_goals": "规划目标...",
|
"planning_goals": "规划目标...",
|
||||||
"breaking_down_tasks": "分解任务...",
|
"breaking_down_tasks": "分解任务...",
|
||||||
|
"generating_delivery": "生成交付内容...",
|
||||||
|
"sending_delivery": "正在发送...",
|
||||||
|
"learning_from_exec": "学习执行经验...",
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"failed_prefix": "失败于",
|
"failed_prefix": "失败于",
|
||||||
"task_prefix": "任务",
|
"task_prefix": "任务",
|
||||||
|
|
@ -451,6 +493,30 @@ func (e *Executor) updateUIFields(ctx *robottypes.Context, exec *robottypes.Exec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateTasksState persists the current tasks array with status to database
|
||||||
|
// This should be called after each task status change for real-time UI updates
|
||||||
|
func (e *Executor) updateTasksState(ctx *robottypes.Context, exec *robottypes.Execution) {
|
||||||
|
if e.config.SkipPersistence || e.store == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert Current to store.CurrentState
|
||||||
|
var current *store.CurrentState
|
||||||
|
if exec.Current != nil {
|
||||||
|
current = &store.CurrentState{
|
||||||
|
TaskIndex: exec.Current.TaskIndex,
|
||||||
|
Progress: exec.Current.Progress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.store.UpdateTasks(ctx.Context, exec.ID, exec.Tasks, current); err != nil {
|
||||||
|
log.With(log.F{
|
||||||
|
"execution_id": exec.ID,
|
||||||
|
"error": err,
|
||||||
|
}).Warn("Failed to update tasks state: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extractGoalName extracts the execution name from goals output
|
// extractGoalName extracts the execution name from goals output
|
||||||
func extractGoalName(goals *robottypes.Goals) string {
|
func extractGoalName(goals *robottypes.Goals) string {
|
||||||
if goals == nil || goals.Content == "" {
|
if goals == nil || goals.Content == "" {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,13 @@ import (
|
||||||
//
|
//
|
||||||
// TODO: Implement real learning extraction
|
// TODO: Implement real learning extraction
|
||||||
func (e *Executor) RunLearning(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
|
func (e *Executor) RunLearning(ctx *robottypes.Context, exec *robottypes.Execution, _ interface{}) error {
|
||||||
|
// Get robot for locale
|
||||||
|
robot := exec.GetRobot()
|
||||||
|
|
||||||
|
// Update UI field with i18n
|
||||||
|
locale := getEffectiveLocale(robot, exec.Input)
|
||||||
|
e.updateUIFields(ctx, exec, "", getLocalizedMessage(locale, "learning_from_exec"))
|
||||||
|
|
||||||
e.simulateStreamDelay()
|
e.simulateStreamDelay()
|
||||||
|
|
||||||
exec.Learning = []robottypes.LearningEntry{
|
exec.Learning = []robottypes.LearningEntry{
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ func (e *Executor) RunExecution(ctx *robottypes.Context, exec *robottypes.Execut
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
task.StartTime = &now
|
task.StartTime = &now
|
||||||
|
|
||||||
|
// Persist running state to database
|
||||||
|
e.updateTasksState(ctx, exec)
|
||||||
|
|
||||||
// Build task context with previous results
|
// Build task context with previous results
|
||||||
taskCtx := runner.BuildTaskContext(exec, i)
|
taskCtx := runner.BuildTaskContext(exec, i)
|
||||||
|
|
||||||
|
|
@ -114,12 +117,17 @@ func (e *Executor) RunExecution(ctx *robottypes.Context, exec *robottypes.Execut
|
||||||
// Store result
|
// Store result
|
||||||
exec.Results = append(exec.Results, *result)
|
exec.Results = append(exec.Results, *result)
|
||||||
|
|
||||||
|
// Persist completed/failed state to database
|
||||||
|
e.updateTasksState(ctx, exec)
|
||||||
|
|
||||||
// Check if we should continue on failure
|
// Check if we should continue on failure
|
||||||
if !result.Success && !config.ContinueOnFailure {
|
if !result.Success && !config.ContinueOnFailure {
|
||||||
// Mark remaining tasks as skipped
|
// Mark remaining tasks as skipped
|
||||||
for j := i + 1; j < len(exec.Tasks); j++ {
|
for j := i + 1; j < len(exec.Tasks); j++ {
|
||||||
exec.Tasks[j].Status = robottypes.TaskSkipped
|
exec.Tasks[j].Status = robottypes.TaskSkipped
|
||||||
}
|
}
|
||||||
|
// Persist skipped state to database
|
||||||
|
e.updateTasksState(ctx, exec)
|
||||||
return fmt.Errorf("task %s failed: %s", task.ID, result.Error)
|
return fmt.Errorf("task %s failed: %s", task.ID, result.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +143,16 @@ func formatTaskProgressName(task *robottypes.Task, index int, total int, locale
|
||||||
taskPrefix := getLocalizedMessage(locale, "task_prefix")
|
taskPrefix := getLocalizedMessage(locale, "task_prefix")
|
||||||
prefix := fmt.Sprintf("%s %d/%d: ", taskPrefix, index+1, total)
|
prefix := fmt.Sprintf("%s %d/%d: ", taskPrefix, index+1, total)
|
||||||
|
|
||||||
// Try to get description from first message
|
// Priority 1: Use Description field if available
|
||||||
|
if task.Description != "" {
|
||||||
|
desc := task.Description
|
||||||
|
if len(desc) > 80 {
|
||||||
|
desc = desc[:80] + "..."
|
||||||
|
}
|
||||||
|
return prefix + desc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Try to get description from first message
|
||||||
if len(task.Messages) > 0 {
|
if len(task.Messages) > 0 {
|
||||||
if content, ok := task.Messages[0].GetContentAsString(); ok && content != "" {
|
if content, ok := task.Messages[0].GetContentAsString(); ok && content != "" {
|
||||||
// Truncate if too long
|
// Truncate if too long
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,11 @@ func ParseTask(data map[string]interface{}, index int) (*robottypes.Task, error)
|
||||||
task.Messages = ParseMessages(messages)
|
task.Messages = ParseMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: description -> convert to message if no messages
|
// Optional: description - save to Description field and convert to message if no messages
|
||||||
if len(task.Messages) == 0 {
|
if desc, ok := data["description"].(string); ok && desc != "" {
|
||||||
if desc, ok := data["description"].(string); ok && desc != "" {
|
task.Description = desc
|
||||||
|
// Also convert to Messages for execution if no explicit messages provided
|
||||||
|
if len(task.Messages) == 0 {
|
||||||
task.Messages = []agentcontext.Message{
|
task.Messages = []agentcontext.Message{
|
||||||
{Role: agentcontext.RoleUser, Content: desc},
|
{Role: agentcontext.RoleUser, Content: desc},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,8 @@ func TestParseTasks(t *testing.T) {
|
||||||
// Second task
|
// Second task
|
||||||
assert.Equal(t, "task-002", tasks[1].ID)
|
assert.Equal(t, "task-002", tasks[1].ID)
|
||||||
assert.Equal(t, "experts.text-writer", tasks[1].ExecutorID)
|
assert.Equal(t, "experts.text-writer", tasks[1].ExecutorID)
|
||||||
assert.Len(t, tasks[1].Messages, 1) // description converted to message
|
assert.Equal(t, "Generate report from analysis", tasks[1].Description) // description saved to field
|
||||||
|
assert.Len(t, tasks[1].Messages, 1) // description also converted to message
|
||||||
assert.Equal(t, 1, tasks[1].Order)
|
assert.Equal(t, 1, tasks[1].Order)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -329,6 +330,62 @@ func TestParseTasks(t *testing.T) {
|
||||||
assert.Equal(t, "task-001", tasks[0].ID)
|
assert.Equal(t, "task-001", tasks[0].ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("saves description to field and preserves explicit messages", func(t *testing.T) {
|
||||||
|
data := []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"id": "task-001",
|
||||||
|
"executor_type": "agent",
|
||||||
|
"executor_id": "experts.summarizer",
|
||||||
|
"description": "Task description for UI",
|
||||||
|
"messages": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"role": "user",
|
||||||
|
"content": "Explicit message content",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := standard.ParseTasks(data)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, tasks, 1)
|
||||||
|
|
||||||
|
// Description should be saved to field
|
||||||
|
assert.Equal(t, "Task description for UI", tasks[0].Description)
|
||||||
|
|
||||||
|
// Explicit messages should be preserved (not overwritten by description)
|
||||||
|
assert.Len(t, tasks[0].Messages, 1)
|
||||||
|
content, ok := tasks[0].Messages[0].GetContentAsString()
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "Explicit message content", content)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("converts description to message when no messages provided", func(t *testing.T) {
|
||||||
|
data := []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"id": "task-001",
|
||||||
|
"executor_type": "agent",
|
||||||
|
"executor_id": "experts.summarizer",
|
||||||
|
"description": "Only description, no messages",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := standard.ParseTasks(data)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, tasks, 1)
|
||||||
|
|
||||||
|
// Description should be saved to field
|
||||||
|
assert.Equal(t, "Only description, no messages", tasks[0].Description)
|
||||||
|
|
||||||
|
// Description should also be converted to message for execution
|
||||||
|
assert.Len(t, tasks[0].Messages, 1)
|
||||||
|
content, ok := tasks[0].Messages[0].GetContentAsString()
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Equal(t, "Only description, no messages", content)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("returns error for missing executor_type", func(t *testing.T) {
|
t.Run("returns error for missing executor_type", func(t *testing.T) {
|
||||||
data := []interface{}{
|
data := []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,9 @@ func TestGetLocalizedMessage(t *testing.T) {
|
||||||
keys := []string{
|
keys := []string{
|
||||||
"preparing", "starting", "scheduled_execution",
|
"preparing", "starting", "scheduled_execution",
|
||||||
"event_prefix", "event_triggered", "analyzing_context",
|
"event_prefix", "event_triggered", "analyzing_context",
|
||||||
"planning_goals", "breaking_down_tasks", "completed",
|
"planning_goals", "breaking_down_tasks",
|
||||||
"failed_prefix", "task_prefix",
|
"generating_delivery", "sending_delivery", "learning_from_exec",
|
||||||
|
"completed", "failed_prefix", "task_prefix",
|
||||||
// Phase names for failure messages
|
// Phase names for failure messages
|
||||||
"phase_inspiration", "phase_goals", "phase_tasks",
|
"phase_inspiration", "phase_goals", "phase_tasks",
|
||||||
"phase_run", "phase_delivery", "phase_learning",
|
"phase_run", "phase_delivery", "phase_learning",
|
||||||
|
|
@ -119,8 +120,9 @@ func TestGetLocalizedMessage(t *testing.T) {
|
||||||
keys := []string{
|
keys := []string{
|
||||||
"preparing", "starting", "scheduled_execution",
|
"preparing", "starting", "scheduled_execution",
|
||||||
"event_prefix", "event_triggered", "analyzing_context",
|
"event_prefix", "event_triggered", "analyzing_context",
|
||||||
"planning_goals", "breaking_down_tasks", "completed",
|
"planning_goals", "breaking_down_tasks",
|
||||||
"failed_prefix", "task_prefix",
|
"generating_delivery", "sending_delivery", "learning_from_exec",
|
||||||
|
"completed", "failed_prefix", "task_prefix",
|
||||||
// Phase names for failure messages
|
// Phase names for failure messages
|
||||||
"phase_inspiration", "phase_goals", "phase_tasks",
|
"phase_inspiration", "phase_goals", "phase_tasks",
|
||||||
"phase_run", "phase_delivery", "phase_learning",
|
"phase_run", "phase_delivery", "phase_learning",
|
||||||
|
|
@ -312,9 +314,26 @@ func TestExtractGoalName(t *testing.T) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
func TestFormatTaskProgressName(t *testing.T) {
|
func TestFormatTaskProgressName(t *testing.T) {
|
||||||
t.Run("formats_with_task_description", func(t *testing.T) {
|
t.Run("prioritizes_description_field_over_messages", func(t *testing.T) {
|
||||||
task := &robottypes.Task{
|
task := &robottypes.Task{
|
||||||
ID: "task-001",
|
ID: "task-001",
|
||||||
|
Description: "High-level task description for UI",
|
||||||
|
ExecutorType: robottypes.ExecutorAssistant,
|
||||||
|
ExecutorID: "analyst",
|
||||||
|
Messages: []agentcontext.Message{
|
||||||
|
{Role: agentcontext.RoleUser, Content: "Detailed message content for execution"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
name := formatTaskProgressName(task, 0, 3, "en")
|
||||||
|
// Should use Description field, NOT the message content
|
||||||
|
assert.Equal(t, "Task 1/3: High-level task description for UI", name)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("falls_back_to_message_when_no_description", func(t *testing.T) {
|
||||||
|
task := &robottypes.Task{
|
||||||
|
ID: "task-001",
|
||||||
|
Description: "", // Empty description
|
||||||
ExecutorType: robottypes.ExecutorAssistant,
|
ExecutorType: robottypes.ExecutorAssistant,
|
||||||
ExecutorID: "analyst",
|
ExecutorID: "analyst",
|
||||||
Messages: []agentcontext.Message{
|
Messages: []agentcontext.Message{
|
||||||
|
|
@ -340,14 +359,31 @@ func TestFormatTaskProgressName(t *testing.T) {
|
||||||
assert.Equal(t, "任务 2/5: 分析销售数据", name)
|
assert.Equal(t, "任务 2/5: 分析销售数据", name)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("truncates_long_description", func(t *testing.T) {
|
t.Run("truncates_long_description_field", 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"
|
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{
|
task := &robottypes.Task{
|
||||||
ID: "task-001",
|
ID: "task-001",
|
||||||
|
Description: longDesc,
|
||||||
|
ExecutorType: robottypes.ExecutorAssistant,
|
||||||
|
ExecutorID: "analyst",
|
||||||
|
Messages: []agentcontext.Message{},
|
||||||
|
}
|
||||||
|
|
||||||
|
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("truncates_long_message_content", func(t *testing.T) {
|
||||||
|
longContent := "This is a very long message content that should be truncated because it exceeds 80 characters which is the maximum length allowed"
|
||||||
|
task := &robottypes.Task{
|
||||||
|
ID: "task-001",
|
||||||
|
Description: "", // No description, will use message
|
||||||
ExecutorType: robottypes.ExecutorAssistant,
|
ExecutorType: robottypes.ExecutorAssistant,
|
||||||
ExecutorID: "analyst",
|
ExecutorID: "analyst",
|
||||||
Messages: []agentcontext.Message{
|
Messages: []agentcontext.Message{
|
||||||
{Role: agentcontext.RoleUser, Content: longDesc},
|
{Role: agentcontext.RoleUser, Content: longContent},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,34 @@ func (s *ExecutionStore) UpdateCurrent(ctx context.Context, executionID string,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateTasks updates the tasks array with current status
|
||||||
|
// This should be called after each task completes to persist status changes
|
||||||
|
func (s *ExecutionStore) UpdateTasks(ctx context.Context, executionID string, tasks []types.Task, current *CurrentState) error {
|
||||||
|
mod := model.Select(s.modelID)
|
||||||
|
if mod == nil {
|
||||||
|
return fmt.Errorf("model %s not found", s.modelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"tasks": tasks,
|
||||||
|
"current": current,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := mod.UpdateWhere(
|
||||||
|
model.QueryParam{
|
||||||
|
Wheres: []model.QueryWhere{
|
||||||
|
{Column: "execution_id", Value: executionID},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
updateData,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update tasks: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateUIFields updates the UI display fields (name and current_task_name)
|
// UpdateUIFields updates the UI display fields (name and current_task_name)
|
||||||
// These fields are updated by executor at each phase for frontend display
|
// 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 {
|
func (s *ExecutionStore) UpdateUIFields(ctx context.Context, executionID string, name string, currentTaskName string) error {
|
||||||
|
|
|
||||||
|
|
@ -604,6 +604,161 @@ func TestExecutionStoreUpdateUIFields(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestExecutionStoreUpdateTasks tests updating tasks array with status
|
||||||
|
func TestExecutionStoreUpdateTasks(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 with initial tasks
|
||||||
|
startTime := time.Now()
|
||||||
|
record := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_tasks_001",
|
||||||
|
MemberID: "member_tasks_001",
|
||||||
|
TeamID: "team_tasks_001",
|
||||||
|
TriggerType: types.TriggerClock,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
Tasks: []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 0},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskPending, Order: 1},
|
||||||
|
{ID: "task_003", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("updates_task_status_to_running", func(t *testing.T) {
|
||||||
|
// Update first task to running
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskRunning, Order: 0},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskPending, Order: 1},
|
||||||
|
{ID: "task_003", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 2},
|
||||||
|
}
|
||||||
|
current := &store.CurrentState{TaskIndex: 0, Progress: "1/3 tasks"}
|
||||||
|
|
||||||
|
err := s.UpdateTasks(ctx, "exec_test_tasks_001", tasks, current)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_tasks_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, saved.Tasks, 3)
|
||||||
|
|
||||||
|
assert.Equal(t, types.TaskRunning, saved.Tasks[0].Status)
|
||||||
|
assert.Equal(t, types.TaskPending, saved.Tasks[1].Status)
|
||||||
|
assert.Equal(t, types.TaskPending, saved.Tasks[2].Status)
|
||||||
|
|
||||||
|
assert.NotNil(t, saved.Current)
|
||||||
|
assert.Equal(t, 0, saved.Current.TaskIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_task_status_to_completed", func(t *testing.T) {
|
||||||
|
// First task completed, second running
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, Order: 0},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskRunning, Order: 1},
|
||||||
|
{ID: "task_003", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 2},
|
||||||
|
}
|
||||||
|
current := &store.CurrentState{TaskIndex: 1, Progress: "2/3 tasks"}
|
||||||
|
|
||||||
|
err := s.UpdateTasks(ctx, "exec_test_tasks_001", tasks, current)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_tasks_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[0].Status)
|
||||||
|
assert.Equal(t, types.TaskRunning, saved.Tasks[1].Status)
|
||||||
|
assert.Equal(t, types.TaskPending, saved.Tasks[2].Status)
|
||||||
|
assert.Equal(t, 1, saved.Current.TaskIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_task_status_to_failed_with_skipped", func(t *testing.T) {
|
||||||
|
// Second task failed, third skipped
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, Order: 0},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskFailed, Order: 1},
|
||||||
|
{ID: "task_003", ExecutorType: types.ExecutorAssistant, Status: types.TaskSkipped, Order: 2},
|
||||||
|
}
|
||||||
|
current := &store.CurrentState{TaskIndex: 1, Progress: "Failed at 2/3"}
|
||||||
|
|
||||||
|
err := s.UpdateTasks(ctx, "exec_test_tasks_001", tasks, current)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_tasks_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[0].Status)
|
||||||
|
assert.Equal(t, types.TaskFailed, saved.Tasks[1].Status)
|
||||||
|
assert.Equal(t, types.TaskSkipped, saved.Tasks[2].Status)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("updates_with_nil_current", func(t *testing.T) {
|
||||||
|
// All tasks completed, no current
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_001", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, Order: 0},
|
||||||
|
{ID: "task_002", ExecutorType: types.ExecutorProcess, Status: types.TaskCompleted, Order: 1},
|
||||||
|
{ID: "task_003", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, Order: 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.UpdateTasks(ctx, "exec_test_tasks_001", tasks, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_tasks_001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[0].Status)
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[1].Status)
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[2].Status)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("preserves_task_description", func(t *testing.T) {
|
||||||
|
// Create a new record with descriptions
|
||||||
|
record2 := &store.ExecutionRecord{
|
||||||
|
ExecutionID: "exec_test_tasks_002",
|
||||||
|
MemberID: "member_tasks_002",
|
||||||
|
TeamID: "team_tasks_002",
|
||||||
|
TriggerType: types.TriggerHuman,
|
||||||
|
Status: types.ExecRunning,
|
||||||
|
Phase: types.PhaseRun,
|
||||||
|
StartTime: &startTime,
|
||||||
|
Tasks: []types.Task{
|
||||||
|
{ID: "task_d01", Description: "Analyze data", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 0},
|
||||||
|
{ID: "task_d02", Description: "Generate report", ExecutorType: types.ExecutorAssistant, Status: types.TaskPending, Order: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := s.Save(ctx, record2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Update status preserving description
|
||||||
|
tasks := []types.Task{
|
||||||
|
{ID: "task_d01", Description: "Analyze data", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, Order: 0},
|
||||||
|
{ID: "task_d02", Description: "Generate report", ExecutorType: types.ExecutorAssistant, Status: types.TaskRunning, Order: 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.UpdateTasks(ctx, "exec_test_tasks_002", tasks, &store.CurrentState{TaskIndex: 1})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved, err := s.Get(ctx, "exec_test_tasks_002")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "Analyze data", saved.Tasks[0].Description)
|
||||||
|
assert.Equal(t, "Generate report", saved.Tasks[1].Description)
|
||||||
|
assert.Equal(t, types.TaskCompleted, saved.Tasks[0].Status)
|
||||||
|
assert.Equal(t, types.TaskRunning, saved.Tasks[1].Status)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TestExecutionStoreDelete tests deleting execution records
|
// TestExecutionStoreDelete tests deleting execution records
|
||||||
func TestExecutionStoreDelete(t *testing.T) {
|
func TestExecutionStoreDelete(t *testing.T) {
|
||||||
if testing.Short() {
|
if testing.Short() {
|
||||||
|
|
|
||||||
|
|
@ -216,10 +216,11 @@ type DeliveryTarget struct {
|
||||||
|
|
||||||
// Task - planned task (structured, for execution)
|
// Task - planned task (structured, for execution)
|
||||||
type Task struct {
|
type Task struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Messages []agentcontext.Message `json:"messages"` // original input (text, images, files)
|
Description string `json:"description,omitempty"` // human-readable task description (for UI display)
|
||||||
GoalRef string `json:"goal_ref,omitempty"` // reference to goal (e.g., "Goal 1")
|
Messages []agentcontext.Message `json:"messages"` // original input (text, images, files)
|
||||||
Source TaskSource `json:"source"` // auto | human | event
|
GoalRef string `json:"goal_ref,omitempty"` // reference to goal (e.g., "Goal 1")
|
||||||
|
Source TaskSource `json:"source"` // auto | human | event
|
||||||
|
|
||||||
// Executor
|
// Executor
|
||||||
ExecutorType ExecutorType `json:"executor_type"`
|
ExecutorType ExecutorType `json:"executor_type"`
|
||||||
|
|
|
||||||
336
data/bindata.go
336
data/bindata.go
File diff suppressed because it is too large
Load diff
|
|
@ -45,6 +45,21 @@
|
||||||
| `current_task_name` | `current_task_name` | ✅ Added |
|
| `current_task_name` | `current_task_name` | ✅ Added |
|
||||||
| - | `job_id` | 🗑️ **Dead field, to be removed** |
|
| - | `job_id` | 🗑️ **Dead field, to be removed** |
|
||||||
|
|
||||||
|
### Task Fields ✅ Aligned
|
||||||
|
|
||||||
|
| Backend (`types.go`) | Frontend (`types.ts`) | Status |
|
||||||
|
|---------------------|----------------------|--------|
|
||||||
|
| `id` | `id` | ✅ |
|
||||||
|
| `description` | `description` | ✅ Added |
|
||||||
|
| `goal_ref` | `goal_ref` | ✅ |
|
||||||
|
| `source` | `source` | ✅ |
|
||||||
|
| `executor_type` | `executor_type` | ✅ |
|
||||||
|
| `executor_id` | `executor_id` | ✅ |
|
||||||
|
| `status` | `status` | ✅ |
|
||||||
|
| `order` | `order` | ✅ |
|
||||||
|
| `start_time` | `start_time` | ✅ |
|
||||||
|
| `end_time` | `end_time` | ✅ |
|
||||||
|
|
||||||
**Action Items:**
|
**Action Items:**
|
||||||
- [x] **Backend**: `Execution` struct - add `Name`, `CurrentTaskName` fields (see Improvement Plan below)
|
- [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**: `RobotConfig` struct - add `DefaultLocale` field (see Improvement Plan below)
|
||||||
|
|
@ -53,6 +68,10 @@
|
||||||
- [x] **Backend**: Executor - update `Name`, `CurrentTaskName` at each phase
|
- [x] **Backend**: Executor - update `Name`, `CurrentTaskName` at each phase
|
||||||
- [x] **Backend**: Store layer - add `UpdateUIFields()` method
|
- [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)
|
- [x] **Backend**: Unit tests for UI fields and i18n (executor/standard/ui_fields_test.go, store/execution_test.go)
|
||||||
|
- [x] **Backend**: `Task` struct - add `Description` field for human-readable task description
|
||||||
|
- [x] **Backend**: `ParseTask()` - save description from LLM output to `Task.Description`
|
||||||
|
- [x] **Frontend**: `Task` type - add `description` field
|
||||||
|
- [x] **Frontend**: Task list display - use `description` as primary title, fallback to `executor_id`
|
||||||
- [ ] **Frontend**: Remove `job_id` field from `types.ts`
|
- [ ] **Frontend**: Remove `job_id` field from `types.ts`
|
||||||
- [ ] **Frontend**: Remove `job_id` mock data from `mock/data.ts`
|
- [ ] **Frontend**: Remove `job_id` mock data from `mock/data.ts`
|
||||||
- [ ] **Frontend**: Use `name` and `current_task_name` directly from API response
|
- [ ] **Frontend**: Use `name` and `current_task_name` directly from API response
|
||||||
|
|
@ -606,44 +625,52 @@ var uiMessages = map[string]map[string]string{
|
||||||
- [x] Store - add `UpdateUIFields()` method
|
- [x] Store - add `UpdateUIFields()` method
|
||||||
- [x] Unit tests for UI fields and i18n
|
- [x] Unit tests for UI fields and i18n
|
||||||
|
|
||||||
**Frontend Cleanup (Pending):**
|
**Frontend Cleanup (Completed):**
|
||||||
- [x] Components already use `exec.id` (no changes needed)
|
- [x] Components already use `exec.id` (no changes needed)
|
||||||
- [ ] Remove `job_id` field from `types.ts`
|
- [x] Remove `job_id` field from `types.ts`
|
||||||
- [ ] Remove `job_id` from `mock/data.ts`
|
- [x] ~~Remove `job_id` from `mock/data.ts`~~ (mock kept for reference, not used)
|
||||||
- [ ] Use `name`/`current_task_name` directly from API response
|
- [x] Use `name`/`current_task_name` directly from API response (string, not `{en, cn}`)
|
||||||
|
|
||||||
#### 5.2 SDK Types (`types.ts`) ⬜
|
#### 5.2 SDK Types (`types.ts`) ✅
|
||||||
|
|
||||||
- [ ] `ExecutionFilter` interface
|
- [x] `ExecutionFilter` interface
|
||||||
- [ ] `Execution` interface (align with backend `ExecutionResponse`)
|
- [x] `ExecutionResponse` interface (align with backend)
|
||||||
- [ ] `ExecutionListResponse` interface
|
- [x] `ExecutionListResponse` interface
|
||||||
- [ ] `ExecutionControlResponse` interface
|
- [x] `ExecutionControlResponse` interface
|
||||||
|
- [x] `ExecStatus`, `TriggerType`, `Phase` type aliases
|
||||||
|
|
||||||
**Deferred to Phase 5 (SSE):**
|
**Deferred to Phase 5 (SSE):**
|
||||||
- [ ] ~~`TriggerRequest` / `TriggerResponse` interfaces~~
|
- [ ] ~~`TriggerRequest` / `TriggerResponse` interfaces~~
|
||||||
- [ ] ~~`InterveneRequest` / `InterveneResponse` interfaces~~
|
- [ ] ~~`InterveneRequest` / `InterveneResponse` interfaces~~
|
||||||
|
|
||||||
#### 5.3 SDK Methods (`robots.ts`) ⬜
|
#### 5.3 SDK Methods (`robots.ts`) ✅
|
||||||
|
|
||||||
- [ ] `ListExecutions(robotId, filter)`
|
- [x] `ListExecutions(robotId, filter)`
|
||||||
- [ ] `GetExecution(robotId, execId)`
|
- [x] `GetExecution(robotId, execId)`
|
||||||
- [ ] `PauseExecution(robotId, execId)`
|
- [x] `PauseExecution(robotId, execId)`
|
||||||
- [ ] `ResumeExecution(robotId, execId)`
|
- [x] `ResumeExecution(robotId, execId)`
|
||||||
- [ ] `CancelExecution(robotId, execId)`
|
- [x] `CancelExecution(robotId, execId)`
|
||||||
|
|
||||||
**Deferred to Phase 5 (SSE):**
|
**Deferred to Phase 5 (SSE):**
|
||||||
- [ ] ~~`Trigger(robotId, data)`~~
|
- [ ] ~~`Trigger(robotId, data)`~~
|
||||||
- [ ] ~~`Intervene(robotId, data)`~~
|
- [ ] ~~`Intervene(robotId, data)`~~
|
||||||
|
|
||||||
#### 5.4 Page Integration ⬜
|
#### 5.4 Page Integration ✅
|
||||||
|
|
||||||
- [ ] ActiveTab: Replace mock with `ListExecutions()` API
|
- [x] ActiveTab: Replace mock with `ListExecutions()` API
|
||||||
- [ ] Filter: `status=running|pending`
|
- [x] Filter: `status=running|pending`
|
||||||
- [ ] Polling: 1-minute interval (60000ms) - will switch to SSE in Phase 6
|
- [x] Polling: 1-minute interval (60000ms) - will switch to SSE in Phase 6
|
||||||
- [ ] HistoryTab: Replace mock with `ListExecutions()` API
|
- [x] HistoryTab: Replace mock with `ListExecutions()` API
|
||||||
- [ ] Filter: `status` filter, `keyword` search
|
- [x] Filter: `status` filter, `keyword` search
|
||||||
- [ ] Pagination: page/pagesize
|
- [x] Pagination: page/pagesize
|
||||||
- [ ] Polling: 1-minute interval for list refresh
|
- [x] Polling: 1-minute interval for list refresh
|
||||||
|
- [x] Execution Detail: Call `GetExecution()` API
|
||||||
|
- [x] Display execution phases and outputs
|
||||||
|
- [x] Display `name` and `current_task_name` from API
|
||||||
|
- [x] Display `error` field for failed executions
|
||||||
|
- [x] Execution controls: Pause/Resume/Cancel buttons (call control APIs)
|
||||||
|
- [x] Auto-refresh while execution is running (5s for running)
|
||||||
|
- [x] useRobots hook extended with execution methods
|
||||||
|
|
||||||
**Deferred to Phase 5 (SSE):**
|
**Deferred to Phase 5 (SSE):**
|
||||||
- [ ] ~~Assign Task Modal: Call `Trigger()` API~~
|
- [ ] ~~Assign Task Modal: Call `Trigger()` API~~
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue