Enhance Robot Execution Management and Documentation

- Updated the DESIGN.md to clarify the relationship between robots and executions, emphasizing that each trigger creates a new execution mapped to a job.Job for monitoring.
- Revised the RobotState struct to include fields for tracking multiple running executions and their IDs, improving concurrency management.
- Enhanced the TECHNICAL.md to reflect the global robot object for static methods, streamlining the API usage for robot management.
- Improved TypeScript interfaces to align with the new execution tracking structure, ensuring consistency across documentation.
- Added detailed comments and examples for job creation and execution handling, clarifying the integration of the job system within the robot's functionality.
This commit is contained in:
Max 2026-01-14 15:21:05 +08:00
parent 8bcf7d2303
commit 3915ac493c
2 changed files with 189 additions and 110 deletions

View file

@ -663,7 +663,9 @@ stateDiagram-v2
### 7.1 Job System
Each agent = 1 Job. Each run = 1 Execution.
**Relationship:** 1 Robot : N Executions (concurrent), 1 Execution = 1 job.Job
Each trigger creates a new Execution, mapped to a `job.Job` for monitoring.
```
┌─────────────────────────────────────────────────────────────────┐
@ -807,55 +809,65 @@ type ExecutionResult struct {
}
type RobotState struct {
MemberID string // member_id from __yao.member
Status RobotStatus // idle | working | paused | error | maintenance
LastRun time.Time
NextRun time.Time
RunningID string // current execution ID if working
MemberID string // member_id from __yao.member
Status RobotStatus // idle | working | paused | error | maintenance
LastRun time.Time
NextRun time.Time
Running int // current running execution count
MaxRunning int // max concurrent executions (from Quota.Max)
RunningIDs []string // list of running execution IDs
}
```
### 8.3 Execution (Uses Job System)
No separate `autonomous_executions` table. Uses existing Job system:
No separate `autonomous_executions` table. Uses existing Job system.
**Each trigger creates a new job.Job:**
```go
// On robot member create - use Once/Cron/Daemon based on clock mode
// On each trigger (clock/human/event), create a new Job
execID := gonanoid.Must()
j, _ := job.Once(job.GOROUTINE, map[string]interface{}{
"job_id": "robot_" + memberID,
"job_id": "robot_exec_" + execID, // unique per execution
"category_id": "autonomous_robot",
"name": member.DisplayName,
"name": fmt.Sprintf("%s - %s", member.DisplayName, triggerType),
"metadata": map[string]interface{}{
"member_id": memberID,
"team_id": teamID,
"trigger_type": triggerType,
"exec_id": execID,
},
})
job.SaveJob(j)
// Add execution with config
exec := &job.Execution{
ExecutionID: gonanoid.Must(),
JobID: j.JobID,
Status: "queued",
TriggerCategory: string(TriggerClock), // or TriggerHuman, TriggerEvent
ExecutionConfig: &job.ExecutionConfig{
Type: job.ExecutionTypeProcess,
ProcessName: "robot.Execute",
ProcessArgs: []interface{}{memberID, triggerData},
},
// Configure and start
j.ExecutionConfig = &job.ExecutionConfig{
Type: job.ExecutionTypeProcess,
ProcessName: "robot.Execute",
ProcessArgs: []interface{}{memberID, execID, triggerData},
}
job.SaveExecution(exec)
// Start execution
j.Push()
```
// Query history
**Query executions for a robot:**
```go
// List all executions for a robot member
param := model.QueryParam{
Wheres: []model.QueryWhere{{Column: "job_id", Value: j.JobID}},
Wheres: []model.QueryWhere{
{Column: "category_id", Value: "autonomous_robot"},
{Column: "metadata->member_id", Value: memberID},
},
Orders: []model.QueryOrder{{Column: "created_at", Option: "desc"}},
}
execs, _ := job.ListExecutions(param, 1, 10)
jobs, _ := job.ListJobs(param, 1, 10)
```
**Query examples:**
```go
// List robot jobs
// List all robot jobs (all robots, all executions)
param := model.QueryParam{
Wheres: []model.QueryWhere{
{Column: "category_id", Value: "autonomous_robot"},

View file

@ -12,7 +12,7 @@ yao/agent/robot/
├── api/ # All API forms
│ ├── api.go # Go API (facade)
│ ├── process.go # Yao Process: robot.*
│ └── jsapi.go # JS API: $robot.*
│ └── jsapi.go # JS API: robot (global) + Robot (class)
├── types/ # Types only (no logic, no external deps)
│ ├── enums.go # Phase, ClockMode, TriggerType, etc.
@ -246,12 +246,12 @@ type RobotState struct {
MemberID string `json:"member_id"`
TeamID string `json:"team_id"`
DisplayName string `json:"display_name"`
Status string `json:"status"` // idle | working | paused | error
Running int `json:"running"` // current running count
MaxRunning int `json:"max_running"` // max concurrent allowed
Status string `json:"status"` // idle | working | paused | error
Running int `json:"running"` // current running execution count
MaxRunning int `json:"max_running"` // max concurrent allowed (from Quota.Max)
LastRun *time.Time `json:"last_run,omitempty"`
NextRun *time.Time `json:"next_run,omitempty"`
CurrentExec string `json:"current_exec,omitempty"` // current execution ID
RunningIDs []string `json:"running_ids,omitempty"` // list of running execution IDs
}
// ==================== Trigger Types ====================
@ -437,21 +437,32 @@ func RobotNew(ctx *v8go.Context, memberID string) (*v8go.Value, error) {
}
```
**Static methods (Robot.List, Robot.Create):**
**Global object `robot` (static methods):**
```go
// Register static methods on Robot constructor
func RegisterStaticMethods(iso *v8go.Isolate, robotFn *v8go.FunctionTemplate) {
robotFn.Set("List", v8go.NewFunctionTemplate(iso, jsListRobots))
robotFn.Set("Create", v8go.NewFunctionTemplate(iso, jsCreateRobot))
robotFn.Set("Get", v8go.NewFunctionTemplate(iso, jsGetRobot))
robotFn.Set("Execution", v8go.NewFunctionTemplate(iso, jsGetExecution))
func init() {
// Register global robot object (lowercase, for static methods)
v8.RegisterObject("robot", ExportObject)
}
// ExportObject exports the robot global object
func ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate {
obj := v8go.NewObjectTemplate(iso)
obj.Set("List", v8go.NewFunctionTemplate(iso, jsList))
obj.Set("Get", v8go.NewFunctionTemplate(iso, jsGet))
obj.Set("Create", v8go.NewFunctionTemplate(iso, jsCreate))
obj.Set("Update", v8go.NewFunctionTemplate(iso, jsUpdate))
obj.Set("Remove", v8go.NewFunctionTemplate(iso, jsRemove))
obj.Set("Execution", v8go.NewFunctionTemplate(iso, jsExecution))
return obj
}
```
**TypeScript Interface:**
```typescript
// ==================== Types ====================
interface RobotData {
member_id: string;
team_id: string;
@ -462,12 +473,14 @@ interface RobotData {
interface RobotState {
member_id: string;
status: string;
running: number;
max_running: number;
team_id: string;
display_name: string;
status: string; // idle | working | paused | error
running: number; // current running execution count
max_running: number; // max concurrent allowed
last_run?: string;
next_run?: string;
current_exec?: string;
running_ids?: string[]; // list of running execution IDs
}
interface TriggerResult {
@ -536,10 +549,29 @@ interface UpdateRequest {
robot_config?: RobotConfig;
}
// Robot instance (created via new Robot(memberID))
// ==================== Global object: robot ====================
// Static methods, no instance needed
interface RobotStatic {
List(query?: ListQuery): ListResult;
Get(memberID: string): RobotData;
Create(teamID: string, data: CreateRequest): RobotData;
Update(memberID: string, data: UpdateRequest): RobotData;
Remove(memberID: string): void;
Execution(execID: string): Execution;
}
declare const robot: RobotStatic;
// ==================== Constructor: Robot ====================
// Instance methods, operate on specific robot
declare class Robot {
constructor(memberID: string);
// Properties
readonly memberID: string;
// Instance methods
Status(): RobotState;
UpdateStatus(status: string): void;
@ -548,69 +580,71 @@ declare class Robot {
Pause(execID: string): void;
Resume(execID: string): void;
Stop(execID: string): void;
// Static methods
static List(query?: ListQuery): ListResult;
static Create(teamID: string, data: CreateRequest): RobotData;
static Get(memberID: string): RobotData;
static Execution(execID: string): Execution;
}
```
**Usage:**
```javascript
// Create robot instance
const robot = new Robot("mem_abc123");
// ==================== Global object: robot ====================
// For CRUD and queries (no instance needed)
const list = robot.List({ team_id: "team_xyz", status: "idle" });
const data = robot.Get("mem_abc123");
const newRobot = robot.Create("team_xyz", {
display_name: "Sales Bot",
robot_config: { ... }
});
robot.Update("mem_abc123", { display_name: "Updated Bot" });
robot.Remove("mem_abc123");
const exec = robot.Execution("exec_456");
// ==================== Constructor: Robot ====================
// For operating on a specific robot instance
const bot = new Robot("mem_abc123");
// Instance methods
const state = robot.Status();
const state = bot.Status();
if (state.status === "idle") {
// Trigger with human intervention
const result = robot.Trigger({
const result = bot.Trigger({
type: "human",
action: "task.add",
description: "Analyze sales data",
insert_at: "first", // urgent task, insert at beginning
insert_at: "first",
});
console.log("Triggered:", result.accepted);
}
// Get execution history
const execs = robot.Executions({ status: "completed", page: 1 });
// Get execution history for this robot
const execs = bot.Executions({ status: "completed", page: 1 });
// Control execution
robot.Pause("exec_123");
robot.Resume("exec_123");
robot.Stop("exec_123");
bot.Pause("exec_123");
bot.Resume("exec_123");
bot.Stop("exec_123");
// Static methods
const list = Robot.List({ team_id: "team_xyz", status: "idle" });
const data = Robot.Get("mem_abc123");
const newRobot = Robot.Create("team_xyz", {
display_name: "Sales Bot",
robot_config: { ... }
});
const exec = Robot.Execution("exec_456");
// Update status
bot.UpdateStatus("paused");
```
**Usage in Agent Hooks:**
```javascript
function Create(ctx, messages) {
const robot = new Robot("mem_abc123");
const state = robot.Status();
const bot = new Robot("mem_abc123");
const state = bot.Status();
if (state.status === "working") {
ctx.Send({ type: "text", props: { content: "Robot is busy" } });
return null;
}
const result = robot.Trigger({
const result = bot.Trigger({
type: "human",
action: "task.add",
description: "Analyze this data",
insert_at: "first", // urgent: insert at beginning
insert_at: "first",
});
if (result.accepted) {
@ -623,7 +657,7 @@ function Create(ctx, messages) {
function Next(ctx, payload) {
const execID = ctx.memory.context.Get("robot_exec_id");
if (execID) {
const exec = Robot.Execution(execID);
const exec = robot.Execution(execID); // use global object
if (exec.status === "completed") {
ctx.Send({
type: "text",
@ -1046,7 +1080,9 @@ import (
"time"
)
// Robot - runtime representation of an autonomous robot
// Robot - runtime representation of an autonomous robot (from __yao.member)
// Relationship: 1 Robot : N Executions (concurrent)
// Each trigger creates a new Execution (mapped to job.Job)
type Robot struct {
// From __yao.member
MemberID string `json:"member_id"`
@ -1056,49 +1092,76 @@ type Robot struct {
Status RobotStatus `json:"robot_status"`
AutonomousMode bool `json:"autonomous_mode"`
// Parsed config
// Parsed config (from robot_config JSON field)
Config *Config `json:"-"`
// Runtime state (job.Job stored as interface{} to avoid import cycle)
Job interface{} `json:"-"` // *job.Job, set by manager
JobID string `json:"-"` // job_id for quick access
LastExecution time.Time `json:"-"`
NextExecution time.Time `json:"-"`
// Runtime state
LastRun time.Time `json:"-"` // last execution start time
NextRun time.Time `json:"-"` // next scheduled execution (for clock trigger)
// Concurrency control
running int
runningMu sync.Mutex
// Each Robot can run multiple Executions concurrently (up to Quota.Max)
executions map[string]*Execution // execID -> Execution
execMu sync.RWMutex
}
// CanRun checks if robot can accept new execution
func (r *Robot) CanRun() bool {
r.runningMu.Lock()
defer r.runningMu.Unlock()
return r.running < r.Config.Quota.GetMax()
r.execMu.RLock()
defer r.execMu.RUnlock()
return len(r.executions) < r.Config.Quota.GetMax()
}
// IncrRunning increments running count
func (r *Robot) IncrRunning() {
r.runningMu.Lock()
defer r.runningMu.Unlock()
r.running++
// RunningCount returns current running execution count
func (r *Robot) RunningCount() int {
r.execMu.RLock()
defer r.execMu.RUnlock()
return len(r.executions)
}
// DecrRunning decrements running count
func (r *Robot) DecrRunning() {
r.runningMu.Lock()
defer r.runningMu.Unlock()
if r.running > 0 {
r.running--
// AddExecution adds an execution to tracking
func (r *Robot) AddExecution(exec *Execution) {
r.execMu.Lock()
defer r.execMu.Unlock()
if r.executions == nil {
r.executions = make(map[string]*Execution)
}
r.executions[exec.ID] = exec
}
// Execution - single execution context
// RemoveExecution removes an execution from tracking
func (r *Robot) RemoveExecution(execID string) {
r.execMu.Lock()
defer r.execMu.Unlock()
delete(r.executions, execID)
}
// GetExecution returns an execution by ID
func (r *Robot) GetExecution(execID string) *Execution {
r.execMu.RLock()
defer r.execMu.RUnlock()
return r.executions[execID]
}
// GetExecutions returns all running executions
func (r *Robot) GetExecutions() []*Execution {
r.execMu.RLock()
defer r.execMu.RUnlock()
execs := make([]*Execution, 0, len(r.executions))
for _, exec := range r.executions {
execs = append(execs, exec)
}
return execs
}
// Execution - single execution instance
// Each trigger creates a new Execution, mapped to a job.Job for monitoring
// Relationship: 1 Execution = 1 job.Job
type Execution struct {
ID string `json:"id"`
MemberID string `json:"member_id"`
ID string `json:"id"` // unique execution ID
MemberID string `json:"member_id"` // robot member ID
TeamID string `json:"team_id"`
TriggerType TriggerType `json:"trigger_type"`
TriggerType TriggerType `json:"trigger_type"` // clock | human | event
TriggerData interface{} `json:"trigger_data,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
@ -1106,19 +1169,22 @@ type Execution struct {
Phase Phase `json:"phase"`
Error string `json:"error,omitempty"`
// Job integration (each Execution = 1 job.Job)
JobID string `json:"job_id"` // corresponding job.Job ID
// Phase outputs
Inspiration *InspirationReport `json:"inspiration,omitempty"`
Goals []Goal `json:"goals,omitempty"` // all goals
Tasks []Task `json:"tasks,omitempty"` // all tasks
Goals []Goal `json:"goals,omitempty"` // all goals
Tasks []Task `json:"tasks,omitempty"` // all tasks
Current *CurrentState `json:"current,omitempty"` // current executing state
Results []TaskResult `json:"results,omitempty"`
Delivery *DeliveryResult `json:"delivery,omitempty"`
Learning []LearningEntry `json:"learning,omitempty"`
// Context
ctx context.Context
cancel context.CancelFunc
robot *Robot
// Runtime (internal, not serialized)
ctx context.Context `json:"-"`
cancel context.CancelFunc `json:"-"`
robot *Robot `json:"-"`
}
// CurrentState - current executing goal and task
@ -1377,10 +1443,11 @@ type RobotState struct {
TeamID string `json:"team_id"`
DisplayName string `json:"display_name"`
Status RobotStatus `json:"status"`
Running int `json:"running"` // current running execution count
MaxRunning int `json:"max_running"` // max concurrent allowed
LastRun *time.Time `json:"last_run,omitempty"`
NextRun *time.Time `json:"next_run,omitempty"`
RunningID string `json:"running_id,omitempty"` // current execution ID
RunningCnt int `json:"running_cnt"` // current running count
RunningIDs []string `json:"running_ids,omitempty"` // list of running execution IDs
}
```