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 ### 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.
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
@ -811,51 +813,61 @@ type RobotState struct {
Status RobotStatus // idle | working | paused | error | maintenance Status RobotStatus // idle | working | paused | error | maintenance
LastRun time.Time LastRun time.Time
NextRun time.Time NextRun time.Time
RunningID string // current execution ID if working 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) ### 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 ```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{}{ j, _ := job.Once(job.GOROUTINE, map[string]interface{}{
"job_id": "robot_" + memberID, "job_id": "robot_exec_" + execID, // unique per execution
"category_id": "autonomous_robot", "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) job.SaveJob(j)
// Add execution with config // Configure and start
exec := &job.Execution{ j.ExecutionConfig = &job.ExecutionConfig{
ExecutionID: gonanoid.Must(),
JobID: j.JobID,
Status: "queued",
TriggerCategory: string(TriggerClock), // or TriggerHuman, TriggerEvent
ExecutionConfig: &job.ExecutionConfig{
Type: job.ExecutionTypeProcess, Type: job.ExecutionTypeProcess,
ProcessName: "robot.Execute", ProcessName: "robot.Execute",
ProcessArgs: []interface{}{memberID, triggerData}, ProcessArgs: []interface{}{memberID, execID, triggerData},
},
} }
job.SaveExecution(exec)
// Start execution
j.Push() j.Push()
```
// Query history **Query executions for a robot:**
```go
// List all executions for a robot member
param := model.QueryParam{ 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:** **Query examples:**
```go ```go
// List robot jobs // List all robot jobs (all robots, all executions)
param := model.QueryParam{ param := model.QueryParam{
Wheres: []model.QueryWhere{ Wheres: []model.QueryWhere{
{Column: "category_id", Value: "autonomous_robot"}, {Column: "category_id", Value: "autonomous_robot"},

View file

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