- Completed the implementation of the P3 Run phase, integrating task execution and validation mechanisms. - Introduced a new `RunConfig` struct to manage execution parameters such as retries and validation thresholds. - Developed a two-layer validation system using the new `yao/assert` package, supporting both natural language and structured JSON rules. - Enhanced the `RunExecution` method to execute tasks sequentially with progress tracking and a retry mechanism for validation failures. - Updated task structures to include comprehensive validation rules and expected outputs, ensuring robust task management. - Added unit tests for the new execution and validation features, achieving high test coverage across the implementation. - Revised documentation to reflect changes in the architecture and functionality of the P3 phase.
1771 lines
60 KiB
Markdown
1771 lines
60 KiB
Markdown
# Robot Agent - Technical Design
|
|
|
|
## 1. Code Structure
|
|
|
|
```
|
|
yao/agent/robot/
|
|
├── DESIGN.md # Product design doc
|
|
├── TECHNICAL.md # This file
|
|
│
|
|
├── robot.go # Package entry, Init(), Shutdown()
|
|
│
|
|
├── api/ # All API forms
|
|
│ ├── api.go # Go API (facade)
|
|
│ ├── process.go # Yao Process: robot.*
|
|
│ └── jsapi.go # JS API: robot (global) + Robot (class)
|
|
│
|
|
├── types/ # Types only (no logic, no external deps)
|
|
│ ├── enums.go # Phase, ClockMode, TriggerType, etc.
|
|
│ ├── config.go # Config, Clock, Identity, Quota, etc.
|
|
│ ├── robot.go # Robot, Execution
|
|
│ ├── task.go # Goal, Task, TaskResult
|
|
│ ├── request.go # InterveneRequest, EventRequest, etc.
|
|
│ ├── inspiration.go # ClockContext, InspirationReport
|
|
│ ├── interfaces.go # All interfaces (Manager, Trigger, etc.)
|
|
│ └── errors.go # Error definitions
|
|
│
|
|
├── manager/ # Manager package (orchestration)
|
|
│ └── manager.go # Manager struct, Start/Stop, Tick
|
|
│
|
|
├── pool/ # Worker pool & task dispatch
|
|
│ ├── pool.go # Pool struct, Submit
|
|
│ ├── queue.go # Priority queue
|
|
│ └── worker.go # Worker goroutines
|
|
│
|
|
├── executor/ # Executor package (pluggable architecture)
|
|
│ ├── executor.go # Factory functions, unified entry
|
|
│ ├── types/
|
|
│ │ ├── types.go # Executor interface, Config types
|
|
│ │ └── helpers.go # Shared helper functions
|
|
│ ├── standard/
|
|
│ │ ├── executor.go # Real Agent execution (production)
|
|
│ │ ├── agent.go # AgentCaller for LLM calls
|
|
│ │ ├── input.go # InputFormatter for prompts
|
|
│ │ ├── inspiration.go # P0: Inspiration phase
|
|
│ │ ├── goals.go # P1: Goals phase
|
|
│ │ ├── tasks.go # P2: Tasks phase
|
|
│ │ ├── run.go # P3: Run phase (main entry)
|
|
│ │ ├── runner.go # P3: Task Runner (execution logic)
|
|
│ │ ├── validator.go # P3: Validator (two-layer validation)
|
|
│ │ ├── delivery.go # P4: Delivery phase
|
|
│ │ └── learning.go # P5: Learning phase
|
|
│ ├── dryrun/
|
|
│ │ └── executor.go # Simulated execution (testing/demo)
|
|
│ └── sandbox/
|
|
│ └── executor.go # Container-isolated (NOT IMPLEMENTED)
|
|
│
|
|
├── utils/ # Utility functions
|
|
│ ├── convert.go # Type conversions (JSON, map, struct)
|
|
│ ├── time.go # Time parsing, formatting, timezone
|
|
│ ├── id.go # ID generation (nanoid, uuid)
|
|
│ └── validate.go # Validation helpers
|
|
│
|
|
├── trigger/ # Trigger utilities (logic in manager/)
|
|
│ ├── trigger.go # Validation helpers, action utilities
|
|
│ ├── clock.go # ClockMatcher (reusable clock matching logic)
|
|
│ └── control.go # ExecutionController (pause/resume/stop)
|
|
│
|
|
├── cache/ # Cache package
|
|
│ ├── cache.go # Cache struct, Get/List
|
|
│ ├── load.go # LoadAll, LoadOne
|
|
│ └── refresh.go # Refresh logic
|
|
│
|
|
├── dedup/ # Deduplication package
|
|
│ ├── dedup.go # Dedup struct
|
|
│ ├── fast.go # Fast in-memory check
|
|
│ └── semantic.go # Semantic check via agent
|
|
│
|
|
├── store/ # Data store package (KB, FS, DB access)
|
|
│ ├── store.go # Store struct, interface
|
|
│ ├── kb.go # Knowledge base operations
|
|
│ ├── fs.go # File system operations
|
|
│ ├── db.go # Database queries
|
|
│ └── learning.go # Learning entry save (to KB)
|
|
│
|
|
├── job/ # Job system integration
|
|
│ ├── job.go # Create/Get job for robot
|
|
│ ├── execution.go # Create/Update execution
|
|
│ └── log.go # Write execution logs
|
|
│
|
|
└── plan/ # Plan queue (deferred tasks)
|
|
├── plan.go # Plan queue struct
|
|
└── schedule.go # Schedule for later
|
|
|
|
yao/assert/ # Universal assertion library (global package)
|
|
├── types.go # Assertion, Result, interfaces
|
|
├── asserter.go # Asserter implementation (8 assertion types)
|
|
└── helpers.go # Utility functions (ExtractPath, ToString, etc.)
|
|
```
|
|
|
|
### Dependency Graph (No Cycles)
|
|
|
|
> **Note:** `trigger/` is a utility package (validation, clock matching, execution control).
|
|
> All trigger logic flows through `manager/`.
|
|
|
|
```
|
|
┌──────────┐
|
|
│ types/ │ (pure types, no deps)
|
|
└────┬─────┘
|
|
│
|
|
┌───────┬───────┬───────┬──────┼──────┬───────┬───────┬───────┐
|
|
│ │ │ │ │ │ │ │ │
|
|
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
|
|
┌───────┐┌───────┐┌───────┐┌──────┐┌────┐┌──────┐┌───────┐┌─────────┐
|
|
│ cache ││ dedup ││ store ││ pool ││job ││ plan ││ utils ││ trigger │
|
|
└───┬───┘└───┬───┘└───┬───┘└──┬───┘└──┬─┘└──────┘└───────┘└────┬────┘
|
|
│ │ │ │ │ │
|
|
└────────┴────────┴───────┴───────┴────────────────────────┘
|
|
│
|
|
▼
|
|
┌────────────┐
|
|
│ executor/ │
|
|
└──────┬─────┘
|
|
│
|
|
▼
|
|
┌────────────┐
|
|
│ manager/ │ (imports trigger/ for utilities)
|
|
└──────┬─────┘
|
|
│
|
|
┌──────────────┴──────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────┐ ┌─────────────┐
|
|
│ robot.go │ │ api/ │
|
|
└─────────────┘ └─────────────┘
|
|
```
|
|
|
|
### Package Dependencies
|
|
|
|
| Package | Imports |
|
|
| ----------- | ----------------------------------------------------------- |
|
|
| `types/` | stdlib only |
|
|
| `utils/` | stdlib only |
|
|
| `cache/` | `types/` |
|
|
| `dedup/` | `types/` |
|
|
| `store/` | `types/` |
|
|
| `pool/` | `types/` |
|
|
| `trigger/` | `types/` |
|
|
| `job/` | `types/`, `yao/job` |
|
|
| `plan/` | `types/` |
|
|
| `executor/` | `types/`, `cache/`, `dedup/`, `store/`, `pool/`, `job/`, `yao/assert` |
|
|
| `manager/` | `types/`, `cache/`, `pool/`, `trigger/`, `executor/` |
|
|
| | Manager handles all trigger logic (clock, intervene, event) |
|
|
| `api/` | `types/`, `manager/` |
|
|
| root | all packages |
|
|
|
|
### Public API (`api/`)
|
|
|
|
Three API forms, all in `api/` directory.
|
|
|
|
#### Go API (`api/api.go`)
|
|
|
|
```go
|
|
package api
|
|
|
|
import (
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
)
|
|
|
|
// ==================== CRUD ====================
|
|
|
|
// Get returns a robot by member ID
|
|
func Get(ctx *types.Context, memberID string) (*types.Robot, error)
|
|
|
|
// List returns robots with pagination and filtering
|
|
func List(ctx *types.Context, query *ListQuery) (*ListResult, error)
|
|
|
|
// Create creates a new robot member
|
|
func Create(ctx *types.Context, teamID string, req *CreateRequest) (*types.Robot, error)
|
|
|
|
// Update updates robot config
|
|
func Update(ctx *types.Context, memberID string, req *UpdateRequest) (*types.Robot, error)
|
|
|
|
// Remove deletes a robot member
|
|
func Remove(ctx *types.Context, memberID string) error
|
|
|
|
// ==================== Status ====================
|
|
|
|
// Status returns current robot runtime state
|
|
func Status(ctx *types.Context, memberID string) (*RobotState, error)
|
|
|
|
// UpdateStatus updates robot status (idle, paused, etc.)
|
|
func UpdateStatus(ctx *types.Context, memberID string, status types.RobotStatus) error
|
|
|
|
// ==================== Trigger ====================
|
|
|
|
// Trigger starts execution with specified trigger type and request
|
|
func Trigger(ctx *types.Context, memberID string, req *TriggerRequest) (*TriggerResult, error)
|
|
|
|
// ==================== Execution ====================
|
|
|
|
// GetExecutions returns execution history
|
|
func GetExecutions(ctx *types.Context, memberID string, query *ExecutionQuery) (*ExecutionResult, error)
|
|
|
|
// GetExecution returns a specific execution by ID
|
|
func GetExecution(ctx *types.Context, execID string) (*types.Execution, error)
|
|
|
|
// Pause pauses a running execution
|
|
func Pause(ctx *types.Context, execID string) error
|
|
|
|
// Resume resumes a paused execution
|
|
func Resume(ctx *types.Context, execID string) error
|
|
|
|
// Stop stops a running execution
|
|
func Stop(ctx *types.Context, execID string) error
|
|
|
|
```
|
|
|
|
#### API Types
|
|
|
|
```go
|
|
// ==================== CRUD Types ====================
|
|
|
|
// CreateRequest - request for Create()
|
|
type CreateRequest struct {
|
|
DisplayName string `json:"display_name"`
|
|
SystemPrompt string `json:"system_prompt,omitempty"`
|
|
Config *types.Config `json:"robot_config"`
|
|
}
|
|
|
|
// UpdateRequest - request for Update()
|
|
type UpdateRequest struct {
|
|
DisplayName *string `json:"display_name,omitempty"`
|
|
SystemPrompt *string `json:"system_prompt,omitempty"`
|
|
Config *types.Config `json:"robot_config,omitempty"`
|
|
}
|
|
|
|
// ListQuery - query options for List()
|
|
type ListQuery struct {
|
|
TeamID string `json:"team_id,omitempty"` // filter by team
|
|
Status types.RobotStatus `json:"status,omitempty"` // idle | working | paused | error
|
|
Keywords string `json:"keywords,omitempty"` // search display_name, role
|
|
ClockMode types.ClockMode `json:"clock_mode,omitempty"` // times | interval | daemon
|
|
Page int `json:"page,omitempty"` // default 1
|
|
PageSize int `json:"pagesize,omitempty"` // default 20, max 100
|
|
Order string `json:"order,omitempty"` // e.g. "created_at desc"
|
|
}
|
|
|
|
// ListResult - result of List()
|
|
type ListResult struct {
|
|
Data []*types.Robot `json:"data"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pagesize"`
|
|
}
|
|
|
|
// RobotState - runtime state from Status()
|
|
type RobotState struct {
|
|
MemberID string `json:"member_id"`
|
|
TeamID string `json:"team_id"`
|
|
DisplayName string `json:"display_name"`
|
|
Status types.RobotStatus `json:"status"` // idle | working | paused | error
|
|
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"`
|
|
RunningIDs []string `json:"running_ids,omitempty"` // list of running execution IDs
|
|
}
|
|
|
|
// ==================== Trigger Types ====================
|
|
|
|
// TriggerRequest - request for Trigger()
|
|
// Input uses []context.Message to support rich content (text, images, files, audio)
|
|
type TriggerRequest struct {
|
|
Type types.TriggerType `json:"type"` // human | event
|
|
|
|
// Human intervention fields (when Type = human)
|
|
Action types.InterventionAction `json:"action,omitempty"` // task.add | goal.adjust | task.cancel | plan.add
|
|
Messages []context.Message `json:"messages,omitempty"` // user's input (supports text, images, files)
|
|
PlanAt *time.Time `json:"plan_at,omitempty"` // for action=plan.add
|
|
InsertAt InsertPosition `json:"insert_at,omitempty"` // where to insert: first | last | next | at
|
|
AtIndex int `json:"at_index,omitempty"` // index when insert_at=at
|
|
|
|
// Event fields (when Type = event)
|
|
Source types.EventSource `json:"source,omitempty"` // webhook | database
|
|
EventType string `json:"event_type,omitempty"` // lead.created, order.paid, etc.
|
|
Data map[string]interface{} `json:"data,omitempty"` // event payload
|
|
|
|
// Executor mode (optional, overrides robot config)
|
|
ExecutorMode types.ExecutorMode `json:"executor_mode,omitempty"` // standard | dryrun
|
|
}
|
|
|
|
// InsertPosition - where to insert task in queue
|
|
type InsertPosition string
|
|
|
|
const (
|
|
InsertFirst InsertPosition = "first" // insert at beginning (highest priority)
|
|
InsertLast InsertPosition = "last" // append at end (default)
|
|
InsertNext InsertPosition = "next" // insert after current task
|
|
InsertAt InsertPosition = "at" // insert at specific index (use AtIndex)
|
|
)
|
|
|
|
// TriggerResult - result of Trigger()
|
|
type TriggerResult struct {
|
|
Accepted bool `json:"accepted"` // whether trigger was accepted
|
|
Queued bool `json:"queued"` // true if queued (quota full)
|
|
Execution *types.Execution `json:"execution,omitempty"` // execution info if started
|
|
JobID string `json:"job_id,omitempty"` // job ID for tracking
|
|
Message string `json:"message,omitempty"` // status message
|
|
}
|
|
|
|
// ==================== Execution Types ====================
|
|
|
|
// ExecutionQuery - query options for GetExecutions()
|
|
type ExecutionQuery struct {
|
|
Status types.ExecStatus `json:"status,omitempty"` // pending | running | completed | failed
|
|
Trigger types.TriggerType `json:"trigger,omitempty"` // clock | human | event
|
|
Page int `json:"page,omitempty"` // default 1
|
|
PageSize int `json:"pagesize,omitempty"`// default 20
|
|
}
|
|
|
|
// ExecutionResult - result of GetExecutions()
|
|
type ExecutionResult struct {
|
|
Data []*types.Execution `json:"data"`
|
|
Total int `json:"total"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pagesize"`
|
|
}
|
|
```
|
|
|
|
#### Process API (`api/process.go`)
|
|
|
|
Yao Process registration. Naming convention: `robot.<Action>`
|
|
|
|
```go
|
|
// Process registration
|
|
func init() {
|
|
process.Register("robot.Get", processGet)
|
|
process.Register("robot.List", processList)
|
|
process.Register("robot.Create", processCreate)
|
|
process.Register("robot.Update", processUpdate)
|
|
process.Register("robot.Remove", processRemove)
|
|
process.Register("robot.Status", processStatus)
|
|
process.Register("robot.UpdateStatus", processUpdateStatus)
|
|
process.Register("robot.Trigger", processTrigger)
|
|
process.Register("robot.Executions", processExecutions)
|
|
process.Register("robot.Execution", processExecution)
|
|
process.Register("robot.Pause", processPause)
|
|
process.Register("robot.Resume", processResume)
|
|
process.Register("robot.Stop", processStop)
|
|
}
|
|
```
|
|
|
|
| Process | Args | Returns | Description |
|
|
| -------------------- | --------------------- | ----------------- | ------------------ |
|
|
| `robot.Get` | `memberID` | `Robot` | Get robot by ID |
|
|
| `robot.List` | `query` | `ListResult` | List robots |
|
|
| `robot.Create` | `teamID`, `data` | `Robot` | Create robot |
|
|
| `robot.Update` | `memberID`, `data` | `Robot` | Update robot |
|
|
| `robot.Remove` | `memberID` | `null` | Delete robot |
|
|
| `robot.Status` | `memberID` | `RobotState` | Get runtime status |
|
|
| `robot.UpdateStatus` | `memberID`, `status` | `null` | Update status |
|
|
| `robot.Trigger` | `memberID`, `request` | `TriggerResult` | Trigger execution |
|
|
| `robot.Executions` | `memberID`, `query` | `ExecutionResult` | List executions |
|
|
| `robot.Execution` | `execID` | `Execution` | Get execution |
|
|
| `robot.Pause` | `execID` | `null` | Pause execution |
|
|
| `robot.Resume` | `execID` | `null` | Resume execution |
|
|
| `robot.Stop` | `execID` | `null` | Stop execution |
|
|
|
|
**Usage:**
|
|
|
|
```javascript
|
|
// In Yao scripts
|
|
const robot = Process("robot.Get", "mem_abc123");
|
|
|
|
const list = Process("robot.List", {
|
|
team_id: "team_xyz",
|
|
status: "idle",
|
|
page: 1,
|
|
pagesize: 20,
|
|
});
|
|
|
|
// Trigger with text message
|
|
const result = Process("robot.Trigger", "mem_abc123", {
|
|
type: "human",
|
|
action: "task.add",
|
|
messages: [
|
|
{ role: "user", content: "Prepare meeting materials for BigCorp" },
|
|
],
|
|
insert_at: "first",
|
|
});
|
|
|
|
// Trigger with image (multimodal)
|
|
const imageResult = Process("robot.Trigger", "mem_abc123", {
|
|
type: "human",
|
|
action: "task.add",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: "Analyze this chart and summarize key trends" },
|
|
{
|
|
type: "image_url",
|
|
image_url: { url: "https://example.com/chart.png" },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
insert_at: "first",
|
|
});
|
|
|
|
// Trigger with event
|
|
const eventResult = Process("robot.Trigger", "mem_abc123", {
|
|
type: "event",
|
|
source: "webhook",
|
|
event_type: "lead.created",
|
|
data: { name: "John", email: "john@example.com" },
|
|
});
|
|
|
|
const execs = Process("robot.Executions", "mem_abc123", {
|
|
status: "completed",
|
|
page: 1,
|
|
});
|
|
```
|
|
|
|
#### JSAPI (`api/jsapi.go`)
|
|
|
|
Register to V8 Runtime using constructor pattern, similar to `new FS()`, `new Store()`, `new Query()`.
|
|
|
|
```go
|
|
func init() {
|
|
// Register Robot constructor
|
|
v8.RegisterFunction("Robot", ExportFunction)
|
|
}
|
|
|
|
// ExportFunction exports the Robot constructor
|
|
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
|
return v8go.NewFunctionTemplate(iso, robotConstructor)
|
|
}
|
|
|
|
// robotConstructor: new Robot(memberID)
|
|
func robotConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
|
ctx := info.Context()
|
|
args := info.Args()
|
|
|
|
if len(args) < 1 {
|
|
return bridge.JsException(ctx, "Robot requires member ID")
|
|
}
|
|
|
|
memberID := args[0].String()
|
|
robotObj, err := RobotNew(ctx, memberID)
|
|
if err != nil {
|
|
return bridge.JsException(ctx, err.Error())
|
|
}
|
|
|
|
return robotObj
|
|
}
|
|
|
|
// RobotNew creates a Robot JS object with methods
|
|
func RobotNew(ctx *v8go.Context, memberID string) (*v8go.Value, error) {
|
|
iso := ctx.Isolate()
|
|
obj := v8go.NewObjectTemplate(iso)
|
|
|
|
// Instance methods (operate on this robot)
|
|
obj.Set("Status", v8go.NewFunctionTemplate(iso, jsStatus))
|
|
obj.Set("UpdateStatus", v8go.NewFunctionTemplate(iso, jsUpdateStatus))
|
|
obj.Set("Trigger", v8go.NewFunctionTemplate(iso, jsTrigger))
|
|
obj.Set("Executions", v8go.NewFunctionTemplate(iso, jsExecutions))
|
|
obj.Set("Pause", v8go.NewFunctionTemplate(iso, jsPause))
|
|
obj.Set("Resume", v8go.NewFunctionTemplate(iso, jsResume))
|
|
obj.Set("Stop", v8go.NewFunctionTemplate(iso, jsStop))
|
|
|
|
// ... create instance with memberID stored
|
|
return obj.NewInstance(ctx)
|
|
}
|
|
```
|
|
|
|
**Global object `robot` (static methods):**
|
|
|
|
```go
|
|
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;
|
|
display_name: string;
|
|
robot_status: "idle" | "working" | "paused" | "error" | "maintenance";
|
|
robot_config: RobotConfig;
|
|
}
|
|
|
|
interface RobotState {
|
|
member_id: string;
|
|
team_id: string;
|
|
display_name: string;
|
|
status: "idle" | "working" | "paused" | "error" | "maintenance";
|
|
running: number; // current running execution count
|
|
max_running: number; // max concurrent allowed
|
|
last_run?: string;
|
|
next_run?: string;
|
|
running_ids?: string[]; // list of running execution IDs
|
|
}
|
|
|
|
interface TriggerResult {
|
|
accepted: boolean;
|
|
queued: boolean;
|
|
execution?: Execution;
|
|
job_id?: string;
|
|
message?: string;
|
|
}
|
|
|
|
// Message - same as context.Message, supports rich content
|
|
interface Message {
|
|
role: "user" | "assistant" | "system" | "tool";
|
|
content: string | ContentPart[];
|
|
name?: string;
|
|
tool_call_id?: string;
|
|
tool_calls?: ToolCall[];
|
|
}
|
|
|
|
interface ContentPart {
|
|
type: "text" | "image_url" | "input_audio" | "file" | "data";
|
|
text?: string;
|
|
image_url?: { url: string; detail?: "auto" | "low" | "high" };
|
|
input_audio?: { data: string; format: string };
|
|
file?: { url: string; name?: string; mime_type?: string };
|
|
data?: { data: string; mime_type: string };
|
|
}
|
|
|
|
interface TriggerRequest {
|
|
type: "human" | "event";
|
|
|
|
// Human intervention fields
|
|
action?:
|
|
| "task.add"
|
|
| "task.cancel"
|
|
| "task.update"
|
|
| "goal.adjust"
|
|
| "goal.add"
|
|
| "goal.complete"
|
|
| "goal.cancel"
|
|
| "plan.add"
|
|
| "plan.remove"
|
|
| "plan.update"
|
|
| "instruct";
|
|
messages?: Message[]; // supports text, images, files, audio
|
|
insert_at?: "first" | "last" | "next" | "at";
|
|
at_index?: number;
|
|
plan_at?: string; // ISO date for plan.add
|
|
|
|
// Event fields
|
|
source?: "webhook" | "database";
|
|
event_type?: string; // lead.created, etc.
|
|
data?: Record<string, any>;
|
|
|
|
// Executor mode (optional, overrides robot config)
|
|
executor_mode?: "standard" | "dryrun"; // sandbox not implemented
|
|
}
|
|
|
|
// ExecutorMode - executor mode type
|
|
type ExecutorMode = "standard" | "dryrun" | "sandbox";
|
|
// Note: "sandbox" requires container infrastructure, falls back to "dryrun"
|
|
|
|
interface ListQuery {
|
|
team_id?: string;
|
|
status?: "idle" | "working" | "paused" | "error" | "maintenance";
|
|
keywords?: string;
|
|
clock_mode?: "times" | "interval" | "daemon";
|
|
page?: number;
|
|
pagesize?: number;
|
|
}
|
|
|
|
interface ListResult {
|
|
data: RobotData[];
|
|
total: number;
|
|
page: number;
|
|
pagesize: number;
|
|
}
|
|
|
|
interface ExecutionQuery {
|
|
status?: "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
trigger?: "clock" | "human" | "event";
|
|
page?: number;
|
|
pagesize?: number;
|
|
}
|
|
|
|
interface ExecutionResult {
|
|
data: Execution[];
|
|
total: number;
|
|
page: number;
|
|
pagesize: number;
|
|
}
|
|
|
|
interface CreateRequest {
|
|
display_name: string;
|
|
system_prompt?: string;
|
|
robot_config: RobotConfig;
|
|
}
|
|
|
|
interface UpdateRequest {
|
|
display_name?: string;
|
|
system_prompt?: string;
|
|
robot_config?: RobotConfig;
|
|
}
|
|
|
|
// ==================== 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;
|
|
Trigger(request: TriggerRequest): TriggerResult;
|
|
Executions(query?: ExecutionQuery): ExecutionResult;
|
|
Pause(execID: string): void;
|
|
Resume(execID: string): void;
|
|
Stop(execID: string): void;
|
|
}
|
|
```
|
|
|
|
**Usage:**
|
|
|
|
```javascript
|
|
// ==================== 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 = bot.Status();
|
|
if (state.status === "idle") {
|
|
const result = bot.Trigger({
|
|
type: "human",
|
|
action: "task.add",
|
|
messages: [{ role: "user", content: "Analyze sales data" }],
|
|
insert_at: "first",
|
|
});
|
|
console.log("Triggered:", result.accepted);
|
|
}
|
|
|
|
// Get execution history for this robot
|
|
const execs = bot.Executions({ status: "completed", page: 1 });
|
|
|
|
// Control execution
|
|
bot.Pause("exec_123");
|
|
bot.Resume("exec_123");
|
|
bot.Stop("exec_123");
|
|
|
|
// Update status
|
|
bot.UpdateStatus("paused");
|
|
```
|
|
|
|
**Usage in Agent Hooks:**
|
|
|
|
```javascript
|
|
function Create(ctx, messages) {
|
|
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 = bot.Trigger({
|
|
type: "human",
|
|
action: "task.add",
|
|
messages: [{ role: "user", content: "Analyze this data" }],
|
|
insert_at: "first",
|
|
});
|
|
|
|
if (result.accepted) {
|
|
ctx.memory.context.Set("robot_exec_id", result.execution.id);
|
|
}
|
|
|
|
return { messages };
|
|
}
|
|
|
|
function Next(ctx, payload) {
|
|
const execID = ctx.memory.context.Get("robot_exec_id");
|
|
if (execID) {
|
|
const exec = robot.Execution(execID); // use global object
|
|
if (exec.status === "completed") {
|
|
ctx.Send({
|
|
type: "text",
|
|
props: { content: `Robot completed: ${exec.delivery?.summary}` },
|
|
});
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Type Definitions
|
|
|
|
> All types are in `robot/types/` package. Other files import as:
|
|
>
|
|
> ```go
|
|
> import "github.com/yaoapp/yao/agent/robot/types"
|
|
> ```
|
|
|
|
### 2.1 Enums
|
|
|
|
```go
|
|
// types/enums.go
|
|
package types
|
|
|
|
// Phase - execution phase
|
|
type Phase string
|
|
|
|
const (
|
|
PhaseInspiration Phase = "inspiration" // P0: Clock only
|
|
PhaseGoals Phase = "goals" // P1
|
|
PhaseTasks Phase = "tasks" // P2
|
|
PhaseRun Phase = "run" // P3
|
|
PhaseDelivery Phase = "delivery" // P4
|
|
PhaseLearning Phase = "learning" // P5
|
|
)
|
|
|
|
// AllPhases for iteration
|
|
var AllPhases = []Phase{
|
|
PhaseInspiration, PhaseGoals, PhaseTasks,
|
|
PhaseRun, PhaseDelivery, PhaseLearning,
|
|
}
|
|
|
|
// ClockMode - clock trigger mode
|
|
type ClockMode string
|
|
|
|
const (
|
|
ClockTimes ClockMode = "times" // run at specific times
|
|
ClockInterval ClockMode = "interval" // run every X duration
|
|
ClockDaemon ClockMode = "daemon" // run continuously
|
|
)
|
|
|
|
// TriggerType - trigger source
|
|
type TriggerType string
|
|
|
|
const (
|
|
TriggerClock TriggerType = "clock"
|
|
TriggerHuman TriggerType = "human"
|
|
TriggerEvent TriggerType = "event"
|
|
)
|
|
|
|
// ExecStatus - execution status
|
|
type ExecStatus string
|
|
|
|
const (
|
|
ExecPending ExecStatus = "pending"
|
|
ExecRunning ExecStatus = "running"
|
|
ExecCompleted ExecStatus = "completed"
|
|
ExecFailed ExecStatus = "failed"
|
|
ExecCancelled ExecStatus = "cancelled"
|
|
)
|
|
|
|
// RobotStatus - matches __yao.member.robot_status
|
|
type RobotStatus string
|
|
|
|
const (
|
|
RobotIdle RobotStatus = "idle"
|
|
RobotWorking RobotStatus = "working"
|
|
RobotPaused RobotStatus = "paused"
|
|
RobotError RobotStatus = "error"
|
|
RobotMaintenance RobotStatus = "maintenance"
|
|
)
|
|
|
|
// InterventionAction - human intervention action
|
|
// Format: category.action (e.g., "task.add", "goal.adjust")
|
|
type InterventionAction string
|
|
|
|
const (
|
|
// Task operations
|
|
ActionTaskAdd InterventionAction = "task.add" // add a new task
|
|
ActionTaskCancel InterventionAction = "task.cancel" // cancel a task
|
|
ActionTaskUpdate InterventionAction = "task.update" // update task details
|
|
|
|
// Goal operations
|
|
ActionGoalAdjust InterventionAction = "goal.adjust" // modify current goal
|
|
ActionGoalAdd InterventionAction = "goal.add" // add a new goal
|
|
ActionGoalComplete InterventionAction = "goal.complete" // mark goal as complete
|
|
ActionGoalCancel InterventionAction = "goal.cancel" // cancel a goal
|
|
|
|
// Plan operations (schedule for later)
|
|
ActionPlanAdd InterventionAction = "plan.add" // add to plan queue
|
|
ActionPlanRemove InterventionAction = "plan.remove" // remove from plan queue
|
|
ActionPlanUpdate InterventionAction = "plan.update" // update planned item
|
|
|
|
// Instruction (direct command)
|
|
ActionInstruct InterventionAction = "instruct" // direct instruction to robot
|
|
)
|
|
|
|
// Priority - task/goal priority
|
|
type Priority string
|
|
|
|
const (
|
|
PriorityHigh Priority = "high"
|
|
PriorityNormal Priority = "normal"
|
|
PriorityLow Priority = "low"
|
|
)
|
|
|
|
// DeliveryType - output delivery type
|
|
type DeliveryType string
|
|
|
|
const (
|
|
DeliveryEmail DeliveryType = "email"
|
|
DeliveryFile DeliveryType = "file"
|
|
DeliveryWebhook DeliveryType = "webhook"
|
|
DeliveryNotify DeliveryType = "notify"
|
|
)
|
|
|
|
// DedupResult - deduplication result
|
|
type DedupResult string
|
|
|
|
const (
|
|
DedupSkip DedupResult = "skip" // skip execution
|
|
DedupMerge DedupResult = "merge" // merge with existing
|
|
DedupProceed DedupResult = "proceed" // proceed normally
|
|
)
|
|
|
|
// EventSource - event trigger source
|
|
type EventSource string
|
|
|
|
const (
|
|
EventWebhook EventSource = "webhook" // HTTP webhook
|
|
EventDatabase EventSource = "database" // DB change trigger
|
|
)
|
|
|
|
// LearningType - learning entry type
|
|
type LearningType string
|
|
|
|
const (
|
|
LearnExecution LearningType = "execution" // execution record
|
|
LearnFeedback LearningType = "feedback" // error/fix feedback
|
|
LearnInsight LearningType = "insight" // pattern/tip insight
|
|
)
|
|
|
|
```
|
|
|
|
### 2.2 Context
|
|
|
|
```go
|
|
// types/context.go
|
|
package types
|
|
|
|
import (
|
|
"context"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
// Context - robot execution context (lightweight)
|
|
type Context struct {
|
|
context.Context // embed standard context
|
|
Auth *types.AuthorizedInfo `json:"auth,omitempty"` // reuse oauth AuthorizedInfo
|
|
MemberID string `json:"member_id,omitempty"` // current robot member ID
|
|
RequestID string `json:"request_id,omitempty"` // request trace ID
|
|
Locale string `json:"locale,omitempty"` // locale (e.g., "en-US")
|
|
}
|
|
|
|
// NewContext creates a new robot context
|
|
func NewContext(parent context.Context, auth *types.AuthorizedInfo) *Context {
|
|
if parent == nil {
|
|
parent = context.Background()
|
|
}
|
|
return &Context{
|
|
Context: parent,
|
|
Auth: auth,
|
|
}
|
|
}
|
|
|
|
// UserID returns user ID from auth
|
|
func (c *Context) UserID() string {
|
|
if c.Auth == nil {
|
|
return ""
|
|
}
|
|
return c.Auth.UserID
|
|
}
|
|
|
|
// TeamID returns team ID from auth
|
|
func (c *Context) TeamID() string {
|
|
if c.Auth == nil {
|
|
return ""
|
|
}
|
|
return c.Auth.TeamID
|
|
}
|
|
```
|
|
|
|
### 2.3 Config Types
|
|
|
|
```go
|
|
// types/config.go
|
|
package types
|
|
|
|
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 *Delivery `json:"delivery,omitempty"`
|
|
Events []Event `json:"events,omitempty"`
|
|
}
|
|
|
|
// Validate validates the config
|
|
func (c *Config) Validate() error {
|
|
if c.Identity == nil || c.Identity.Role == "" {
|
|
return ErrMissingIdentity
|
|
}
|
|
if c.Clock != nil {
|
|
if err := c.Clock.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Triggers - trigger enable/disable
|
|
type Triggers struct {
|
|
Clock *TriggerSwitch `json:"clock,omitempty"`
|
|
Intervene *TriggerSwitch `json:"intervene,omitempty"`
|
|
Event *TriggerSwitch `json:"event,omitempty"`
|
|
}
|
|
|
|
type TriggerSwitch struct {
|
|
Enabled bool `json:"enabled"`
|
|
Actions []string `json:"actions,omitempty"` // for intervene
|
|
}
|
|
|
|
// IsEnabled checks if trigger is enabled (default: true)
|
|
func (t *Triggers) IsEnabled(typ TriggerType) bool {
|
|
if t == nil {
|
|
return true
|
|
}
|
|
switch typ {
|
|
case TriggerClock:
|
|
return t.Clock == nil || t.Clock.Enabled
|
|
case TriggerHuman:
|
|
return t.Intervene == nil || t.Intervene.Enabled
|
|
case TriggerEvent:
|
|
return t.Event == nil || t.Event.Enabled
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Clock - when to wake up
|
|
type Clock struct {
|
|
Mode ClockMode `json:"mode"` // times | interval | daemon
|
|
Times []string `json:"times,omitempty"` // ["09:00", "14:00"]
|
|
Days []string `json:"days,omitempty"` // ["Mon", "Tue"] or ["*"]
|
|
Every string `json:"every,omitempty"` // "30m", "1h"
|
|
TZ string `json:"tz,omitempty"` // "Asia/Shanghai"
|
|
Timeout string `json:"timeout,omitempty"` // "30m"
|
|
}
|
|
|
|
// Validate validates clock config
|
|
func (c *Clock) Validate() error {
|
|
switch c.Mode {
|
|
case ClockTimes:
|
|
if len(c.Times) == 0 {
|
|
return ErrClockTimesEmpty
|
|
}
|
|
case ClockInterval:
|
|
if c.Every == "" {
|
|
return ErrClockIntervalEmpty
|
|
}
|
|
case ClockDaemon:
|
|
// no extra validation
|
|
default:
|
|
return ErrClockModeInvalid
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetTimeout returns parsed timeout duration
|
|
func (c *Clock) GetTimeout() time.Duration {
|
|
if c.Timeout == "" {
|
|
return 30 * time.Minute // default
|
|
}
|
|
d, err := time.ParseDuration(c.Timeout)
|
|
if err != nil {
|
|
return 30 * time.Minute
|
|
}
|
|
return d
|
|
}
|
|
|
|
// GetLocation returns timezone location
|
|
func (c *Clock) GetLocation() *time.Location {
|
|
if c.TZ == "" {
|
|
return time.Local
|
|
}
|
|
loc, err := time.LoadLocation(c.TZ)
|
|
if err != nil {
|
|
return time.Local
|
|
}
|
|
return loc
|
|
}
|
|
|
|
// Identity - who is this robot
|
|
type Identity struct {
|
|
Role string `json:"role"`
|
|
Duties []string `json:"duties,omitempty"`
|
|
Rules []string `json:"rules,omitempty"`
|
|
}
|
|
|
|
// Quota - concurrency limits
|
|
type Quota struct {
|
|
Max int `json:"max"` // max running (default: 2)
|
|
Queue int `json:"queue"` // queue size (default: 10)
|
|
Priority int `json:"priority"` // 1-10 (default: 5)
|
|
}
|
|
|
|
// GetMax returns max with default
|
|
func (q *Quota) GetMax() int {
|
|
if q == nil || q.Max <= 0 {
|
|
return 2
|
|
}
|
|
return q.Max
|
|
}
|
|
|
|
// GetQueue returns queue size with default
|
|
func (q *Quota) GetQueue() int {
|
|
if q == nil || q.Queue <= 0 {
|
|
return 10
|
|
}
|
|
return q.Queue
|
|
}
|
|
|
|
// GetPriority returns priority with default
|
|
func (q *Quota) GetPriority() int {
|
|
if q == nil || q.Priority <= 0 {
|
|
return 5
|
|
}
|
|
return q.Priority
|
|
}
|
|
|
|
// KB - knowledge base config (same as assistant, from store/types)
|
|
// Shared KB collections accessible by this robot
|
|
type KB struct {
|
|
Collections []string `json:"collections,omitempty"` // KB collection IDs
|
|
Options map[string]interface{} `json:"options,omitempty"`
|
|
}
|
|
|
|
// DB - database config (same as assistant, from store/types)
|
|
// Shared database models accessible by this robot
|
|
type DB struct {
|
|
Models []string `json:"models,omitempty"` // database model names
|
|
Options map[string]interface{} `json:"options,omitempty"`
|
|
}
|
|
|
|
// Learn - learning config for robot's private KB
|
|
// Private KB is auto-created: robot_{team_id}_{member_id}_kb
|
|
type Learn struct {
|
|
On bool `json:"on"`
|
|
Types []string `json:"types,omitempty"` // execution, feedback, insight
|
|
Keep int `json:"keep,omitempty"` // days, 0 = forever
|
|
}
|
|
|
|
// Resources - available agents and tools
|
|
type Resources struct {
|
|
Phases map[Phase]string `json:"phases,omitempty"` // phase -> agent ID
|
|
Agents []string `json:"agents,omitempty"`
|
|
MCP []MCPConfig `json:"mcp,omitempty"`
|
|
}
|
|
|
|
// GetPhaseAgent returns agent ID for phase (default: __yao.{phase})
|
|
func (r *Resources) GetPhaseAgent(phase Phase) string {
|
|
if r != nil && r.Phases != nil {
|
|
if id, ok := r.Phases[phase]; ok && id != "" {
|
|
return id
|
|
}
|
|
}
|
|
return "__yao." + string(phase)
|
|
}
|
|
|
|
type MCPConfig struct {
|
|
ID string `json:"id"`
|
|
Tools []string `json:"tools,omitempty"` // empty = all
|
|
}
|
|
|
|
// Delivery - output delivery
|
|
type Delivery struct {
|
|
Type DeliveryType `json:"type"`
|
|
Opts map[string]interface{} `json:"opts,omitempty"`
|
|
}
|
|
|
|
// Event - event trigger config
|
|
type Event struct {
|
|
Type EventSource `json:"type"` // webhook | database
|
|
Source string `json:"source"` // webhook path or table name
|
|
Filter map[string]interface{} `json:"filter,omitempty"`
|
|
}
|
|
|
|
// Monitor - monitoring config
|
|
```
|
|
|
|
### 2.3 Core Types
|
|
|
|
```go
|
|
// types/robot.go
|
|
package types
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// 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"`
|
|
TeamID string `json:"team_id"`
|
|
DisplayName string `json:"display_name"`
|
|
SystemPrompt string `json:"system_prompt"`
|
|
Status RobotStatus `json:"robot_status"`
|
|
AutonomousMode bool `json:"autonomous_mode"`
|
|
|
|
// Parsed config (from robot_config JSON field)
|
|
Config *Config `json:"-"`
|
|
|
|
// Runtime state
|
|
LastRun time.Time `json:"-"` // last execution start time
|
|
NextRun time.Time `json:"-"` // next scheduled execution (for clock trigger)
|
|
|
|
// Concurrency control
|
|
// 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.execMu.RLock()
|
|
defer r.execMu.RUnlock()
|
|
return len(r.executions) < r.Config.Quota.GetMax()
|
|
}
|
|
|
|
// RunningCount returns current running execution count
|
|
func (r *Robot) RunningCount() int {
|
|
r.execMu.RLock()
|
|
defer r.execMu.RUnlock()
|
|
return len(r.executions)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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"` // unique execution ID
|
|
MemberID string `json:"member_id"` // robot member ID
|
|
TeamID string `json:"team_id"`
|
|
TriggerType TriggerType `json:"trigger_type"` // clock | human | event
|
|
StartTime time.Time `json:"start_time"`
|
|
EndTime *time.Time `json:"end_time,omitempty"`
|
|
Status ExecStatus `json:"status"`
|
|
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
|
|
|
|
// Trigger input (stored for traceability)
|
|
Input *TriggerInput `json:"input,omitempty"` // original trigger input
|
|
|
|
// Phase outputs
|
|
Inspiration *InspirationReport `json:"inspiration,omitempty"` // P0: markdown
|
|
Goals *Goals `json:"goals,omitempty"` // P1: markdown
|
|
Tasks []Task `json:"tasks,omitempty"` // P2: structured tasks
|
|
Current *CurrentState `json:"current,omitempty"` // current executing state
|
|
Results []TaskResult `json:"results,omitempty"` // P3: task results
|
|
Delivery *DeliveryResult `json:"delivery,omitempty"`
|
|
Learning []LearningEntry `json:"learning,omitempty"`
|
|
|
|
// Runtime (internal, not serialized)
|
|
ctx context.Context `json:"-"`
|
|
cancel context.CancelFunc `json:"-"`
|
|
robot *Robot `json:"-"`
|
|
}
|
|
|
|
// TriggerInput - stored trigger input for traceability
|
|
type TriggerInput struct {
|
|
// For human intervention
|
|
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
|
|
|
|
// For event trigger
|
|
Source EventSource `json:"source,omitempty"` // webhook | database
|
|
EventType string `json:"event_type,omitempty"` // lead.created, etc.
|
|
Data map[string]interface{} `json:"data,omitempty"` // event payload
|
|
|
|
// For clock trigger
|
|
Clock *ClockContext `json:"clock,omitempty"` // time context when triggered
|
|
}
|
|
|
|
// CurrentState - current executing goal and task
|
|
type CurrentState struct {
|
|
Task *Task `json:"task,omitempty"` // current task being executed
|
|
TaskIndex int `json:"task_index"` // index in Tasks slice
|
|
Progress string `json:"progress,omitempty"` // human-readable progress (e.g., "2/5 tasks")
|
|
}
|
|
|
|
// Goals - P1 output (markdown for LLM + structured metadata)
|
|
// P1 Agent reads InspirationReport and generates goals as markdown
|
|
// Example:
|
|
// ## Goals
|
|
// 1. [High] Analyze sales data and identify trends
|
|
// - Reason: Sales up 50%, need to understand why
|
|
// 2. [Normal] Prepare weekly report for manager
|
|
// - Reason: Friday 5pm, weekly report due
|
|
// 3. [Low] Update CRM with new leads
|
|
// - Reason: 3 pending leads from yesterday
|
|
type Goals struct {
|
|
Content string `json:"content"` // markdown text
|
|
Delivery *DeliveryTarget `json:"delivery,omitempty"` // where to send results (for P4)
|
|
}
|
|
|
|
// DeliveryTarget - where to deliver results (defined in P1, used in P4)
|
|
type DeliveryTarget struct {
|
|
Type DeliveryType `json:"type"` // email | webhook | report | notification
|
|
Recipients []string `json:"recipients,omitempty"` // email addresses, webhook URLs, user IDs
|
|
Format string `json:"format,omitempty"` // markdown | html | json | text
|
|
Template string `json:"template,omitempty"` // template name or inline template
|
|
Options map[string]interface{} `json:"options,omitempty"` // channel-specific options
|
|
}
|
|
|
|
// Task - planned task (structured, for execution)
|
|
type Task struct {
|
|
ID string `json:"id"`
|
|
Messages []context.Message `json:"messages"` // original input (text, images, files)
|
|
GoalRef string `json:"goal_ref,omitempty"` // reference to goal (e.g., "Goal 1")
|
|
Source TaskSource `json:"source"` // auto | human | event
|
|
|
|
// Executor
|
|
ExecutorType ExecutorType `json:"executor_type"`
|
|
ExecutorID string `json:"executor_id"`
|
|
Args []any `json:"args,omitempty"`
|
|
|
|
// Validation (defined in P2, used in P3)
|
|
ExpectedOutput string `json:"expected_output,omitempty"` // what the task should produce
|
|
// ValidationRules supports two formats:
|
|
// 1. Natural language: "output must be valid JSON", "must contain 'field'"
|
|
// 2. JSON assertions: `{"type": "type", "value": "object"}`, `{"type": "contains", "value": "success"}`
|
|
ValidationRules []string `json:"validation_rules,omitempty"` // specific checks to perform
|
|
|
|
// Runtime
|
|
Status TaskStatus `json:"status"`
|
|
Order int `json:"order"` // execution order (0-based)
|
|
StartTime *time.Time `json:"start_time,omitempty"`
|
|
EndTime *time.Time `json:"end_time,omitempty"`
|
|
}
|
|
|
|
// TaskSource - how task was created
|
|
type TaskSource string
|
|
|
|
const (
|
|
TaskSourceAuto TaskSource = "auto" // generated by P2 (task planning)
|
|
TaskSourceHuman TaskSource = "human" // added via human intervention
|
|
TaskSourceEvent TaskSource = "event" // added via event trigger
|
|
)
|
|
|
|
// ExecutorType - task executor type
|
|
type ExecutorType string
|
|
|
|
const (
|
|
ExecutorAssistant ExecutorType = "assistant"
|
|
ExecutorMCP ExecutorType = "mcp"
|
|
ExecutorProcess ExecutorType = "process"
|
|
)
|
|
|
|
// TaskStatus - task execution status
|
|
type TaskStatus string
|
|
|
|
const (
|
|
TaskPending TaskStatus = "pending"
|
|
TaskRunning TaskStatus = "running"
|
|
TaskCompleted TaskStatus = "completed"
|
|
TaskFailed TaskStatus = "failed"
|
|
TaskSkipped TaskStatus = "skipped"
|
|
TaskCancelled TaskStatus = "cancelled"
|
|
)
|
|
|
|
// TaskResult - task execution result
|
|
type TaskResult struct {
|
|
TaskID string `json:"task_id"`
|
|
Success bool `json:"success"`
|
|
Output interface{} `json:"output,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
Duration int64 `json:"duration_ms"`
|
|
Validation *ValidationResult `json:"validation,omitempty"` // P3 validation result
|
|
}
|
|
|
|
// ValidationResult - P3 semantic validation result
|
|
type ValidationResult struct {
|
|
Passed bool `json:"passed"` // overall validation passed
|
|
Score float64 `json:"score,omitempty"` // 0-1 confidence score
|
|
Issues []string `json:"issues,omitempty"` // what failed
|
|
Suggestions []string `json:"suggestions,omitempty"` // how to improve
|
|
Details string `json:"details,omitempty"` // detailed validation report
|
|
}
|
|
|
|
// DeliveryResult - P4 delivery output
|
|
type DeliveryResult struct {
|
|
Type DeliveryType `json:"type"`
|
|
Success bool `json:"success"`
|
|
Recipients []string `json:"recipients,omitempty"` // who received
|
|
Content string `json:"content,omitempty"` // formatted content delivered
|
|
Details interface{} `json:"details,omitempty"` // channel-specific response
|
|
Error string `json:"error,omitempty"`
|
|
SentAt *time.Time `json:"sent_at,omitempty"`
|
|
}
|
|
|
|
// LearningEntry - knowledge to save
|
|
type LearningEntry struct {
|
|
Type LearningType `json:"type"` // execution | feedback | insight
|
|
Content string `json:"content"`
|
|
Tags []string `json:"tags,omitempty"`
|
|
Meta interface{} `json:"meta,omitempty"`
|
|
}
|
|
```
|
|
|
|
### 2.4 Clock Context
|
|
|
|
```go
|
|
// types/clock.go
|
|
package types
|
|
|
|
import "time"
|
|
|
|
// ClockContext - time context for P0 inspiration
|
|
type ClockContext struct {
|
|
Now time.Time `json:"now"`
|
|
Hour int `json:"hour"` // 0-23
|
|
DayOfWeek string `json:"day_of_week"` // Monday, Tuesday...
|
|
DayOfMonth int `json:"day_of_month"` // 1-31
|
|
WeekOfYear int `json:"week_of_year"` // 1-52
|
|
Month int `json:"month"` // 1-12
|
|
Year int `json:"year"`
|
|
IsWeekend bool `json:"is_weekend"`
|
|
IsMonthStart bool `json:"is_month_start"` // 1st-3rd
|
|
IsMonthEnd bool `json:"is_month_end"` // last 3 days
|
|
IsQuarterEnd bool `json:"is_quarter_end"`
|
|
IsYearEnd bool `json:"is_year_end"`
|
|
TZ string `json:"tz"`
|
|
}
|
|
|
|
// NewClockContext creates clock context from time
|
|
func NewClockContext(t time.Time, tz string) *ClockContext {
|
|
loc := time.Local
|
|
if tz != "" {
|
|
if l, err := time.LoadLocation(tz); err == nil {
|
|
loc = l
|
|
}
|
|
}
|
|
t = t.In(loc)
|
|
|
|
_, week := t.ISOWeek()
|
|
dayOfMonth := t.Day()
|
|
lastDay := time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, loc).Day()
|
|
|
|
return &ClockContext{
|
|
Now: t,
|
|
Hour: t.Hour(),
|
|
DayOfWeek: t.Weekday().String(),
|
|
DayOfMonth: dayOfMonth,
|
|
WeekOfYear: week,
|
|
Month: int(t.Month()),
|
|
Year: t.Year(),
|
|
IsWeekend: t.Weekday() == time.Saturday || t.Weekday() == time.Sunday,
|
|
IsMonthStart: dayOfMonth <= 3,
|
|
IsMonthEnd: dayOfMonth >= lastDay-2,
|
|
IsQuarterEnd: (t.Month()%3 == 0) && dayOfMonth >= lastDay-2,
|
|
IsYearEnd: t.Month() == 12 && dayOfMonth >= 29,
|
|
TZ: loc.String(),
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2.5 Inspiration Report
|
|
|
|
```go
|
|
// types/inspiration.go
|
|
package types
|
|
|
|
// InspirationReport - P0 output (simple markdown for LLM)
|
|
type InspirationReport struct {
|
|
Clock *ClockContext `json:"clock"` // time context
|
|
Content string `json:"content"` // markdown text for LLM
|
|
}
|
|
|
|
// Content is markdown like:
|
|
// ## Summary
|
|
// ...
|
|
// ## Highlights
|
|
// - [High] Sales up 50%
|
|
// - [Medium] New lead from BigCorp
|
|
// ## Opportunities
|
|
// ...
|
|
// ## Risks
|
|
// ...
|
|
// ## World News
|
|
// ...
|
|
// ## Pending
|
|
// ...
|
|
```
|
|
|
|
### 2.6 Request/Response Types
|
|
|
|
```go
|
|
// types/request.go
|
|
package types
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// InterveneRequest - human intervention request
|
|
// Processed by Manager.Intervene()
|
|
type InterveneRequest struct {
|
|
TeamID string `json:"team_id"`
|
|
MemberID string `json:"member_id"`
|
|
Action InterventionAction `json:"action"` // task.add, goal.adjust, etc.
|
|
Messages []agentcontext.Message `json:"messages,omitempty"` // user input (text, images, files)
|
|
PlanTime *time.Time `json:"plan_time,omitempty"` // for action=plan.add
|
|
ExecutorMode ExecutorMode `json:"executor_mode,omitempty"` // optional: standard | dryrun
|
|
}
|
|
|
|
// EventRequest - event trigger request
|
|
// Processed by Manager.HandleEvent()
|
|
type EventRequest struct {
|
|
MemberID string `json:"member_id"`
|
|
Source string `json:"source"` // webhook path or table name
|
|
EventType string `json:"event_type"` // lead.created, etc.
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
ExecutorMode ExecutorMode `json:"executor_mode,omitempty"` // optional: standard | dryrun
|
|
}
|
|
|
|
// ExecutorMode - executor mode enum
|
|
type ExecutorMode string
|
|
|
|
const (
|
|
ExecutorStandard ExecutorMode = "standard" // real Agent calls (default)
|
|
ExecutorDryRun ExecutorMode = "dryrun" // simulated, no LLM calls
|
|
ExecutorSandbox ExecutorMode = "sandbox" // container-isolated (NOT IMPLEMENTED)
|
|
)
|
|
|
|
// ExecutionResult - trigger result
|
|
type ExecutionResult struct {
|
|
ExecutionID string `json:"execution_id"`
|
|
Status ExecStatus `json:"status"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// RobotState - robot status query result
|
|
type RobotState struct {
|
|
MemberID string `json:"member_id"`
|
|
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"`
|
|
RunningIDs []string `json:"running_ids,omitempty"` // list of running execution IDs
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Interfaces
|
|
|
|
> Interfaces are also in `types/` package to avoid cycles.
|
|
|
|
### 3.1 Manager Interface
|
|
|
|
```go
|
|
// types/interfaces.go
|
|
package types
|
|
|
|
import "time"
|
|
|
|
// ==================== Internal Interfaces ====================
|
|
// These are internal implementation interfaces, not exposed via API.
|
|
// External API is defined in api/api.go
|
|
// All interfaces use *Context (not context.Context) for consistency.
|
|
|
|
// Manager - robot lifecycle, scheduling, and all trigger handling
|
|
// Manager is the central orchestrator, handling:
|
|
// - Clock triggers (via Tick)
|
|
// - Human intervention (via Intervene)
|
|
// - Event triggers (via HandleEvent)
|
|
// - Execution control (pause/resume/stop)
|
|
type Manager interface {
|
|
// Lifecycle
|
|
Start() error
|
|
Stop() error
|
|
|
|
// Clock trigger (called by internal ticker)
|
|
Tick(ctx *Context, now time.Time) error
|
|
|
|
// Manual trigger (for testing/API)
|
|
TriggerManual(ctx *Context, memberID string, trigger TriggerType, data interface{}) (string, error)
|
|
|
|
// Human intervention
|
|
Intervene(ctx *Context, req *InterveneRequest) (*ExecutionResult, error)
|
|
|
|
// Event trigger
|
|
HandleEvent(ctx *Context, req *EventRequest) (*ExecutionResult, error)
|
|
|
|
// Execution control
|
|
PauseExecution(ctx *Context, execID string) error
|
|
ResumeExecution(ctx *Context, execID string) error
|
|
StopExecution(ctx *Context, execID string) error
|
|
}
|
|
|
|
// Executor - executes robot phases
|
|
type Executor interface {
|
|
Execute(ctx *Context, robot *Robot, trigger TriggerType, data interface{}) (*Execution, error)
|
|
}
|
|
|
|
// Pool - worker pool for concurrent execution
|
|
type Pool interface {
|
|
Start() error
|
|
Stop() error
|
|
Submit(ctx *Context, robot *Robot, trigger TriggerType, data interface{}) (string, error)
|
|
Running() int
|
|
Queued() int
|
|
}
|
|
|
|
// Cache - in-memory robot cache
|
|
type Cache interface {
|
|
Load(ctx *Context) error
|
|
Get(memberID string) *Robot
|
|
List(teamID string) []*Robot
|
|
Refresh(ctx *Context, memberID string) error
|
|
Add(robot *Robot)
|
|
Remove(memberID string)
|
|
}
|
|
|
|
// Dedup - deduplication check
|
|
type Dedup interface {
|
|
Check(ctx *Context, memberID string, trigger TriggerType) (DedupResult, error)
|
|
Mark(memberID string, trigger TriggerType, window time.Duration)
|
|
}
|
|
|
|
// Store - data storage operations (KB, DB)
|
|
type Store interface {
|
|
SaveLearning(ctx *Context, memberID string, entries []LearningEntry) error
|
|
GetHistory(ctx *Context, memberID string, limit int) ([]LearningEntry, error)
|
|
SearchKB(ctx *Context, collections []string, query string) ([]interface{}, error)
|
|
QueryDB(ctx *Context, models []string, query interface{}) ([]interface{}, error)
|
|
}
|
|
```
|
|
|
|
### 3.2 Trigger Utilities (`trigger/` package)
|
|
|
|
> **Note:** The `trigger/` package provides utilities, not the main trigger logic.
|
|
> All trigger handling is done by `Manager`.
|
|
|
|
```go
|
|
// trigger/trigger.go - Validation and helper functions
|
|
|
|
// ValidateIntervention validates a human intervention request
|
|
func ValidateIntervention(req *InterveneRequest) error
|
|
|
|
// ValidateEvent validates an event trigger request
|
|
func ValidateEvent(req *EventRequest) error
|
|
|
|
// BuildEventInput creates a TriggerInput from an event request
|
|
func BuildEventInput(req *EventRequest) *TriggerInput
|
|
|
|
// GetActionCategory returns the category of an intervention action
|
|
// e.g., "task.add" -> "task", "goal.adjust" -> "goal"
|
|
func GetActionCategory(action InterventionAction) string
|
|
|
|
// GetActionDescription returns a human-readable description of an action
|
|
func GetActionDescription(action InterventionAction) string
|
|
```
|
|
|
|
```go
|
|
// trigger/clock.go - Clock matching logic (reusable)
|
|
|
|
// ClockMatcher provides clock trigger matching logic
|
|
type ClockMatcher struct{}
|
|
|
|
// ShouldTrigger checks if a robot should be triggered based on its clock config
|
|
func (cm *ClockMatcher) ShouldTrigger(robot *Robot, now time.Time) bool
|
|
|
|
// ParseTime parses a time string in "HH:MM" format
|
|
func ParseTime(timeStr string) (hour, minute int, err error)
|
|
|
|
// FormatTime formats hour and minute to "HH:MM" string
|
|
func FormatTime(hour, minute int) string
|
|
```
|
|
|
|
```go
|
|
// trigger/control.go - Execution control (pause/resume/stop)
|
|
|
|
// ExecutionController manages execution lifecycle
|
|
type ExecutionController struct {
|
|
executions map[string]*ControlledExecution
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// Track starts tracking an execution
|
|
func (c *ExecutionController) Track(execID, memberID, teamID string) *ControlledExecution
|
|
|
|
// Untrack stops tracking an execution
|
|
func (c *ExecutionController) Untrack(execID string)
|
|
|
|
// Pause pauses an execution
|
|
func (c *ExecutionController) Pause(execID string) error
|
|
|
|
// Resume resumes a paused execution
|
|
func (c *ExecutionController) Resume(execID string) error
|
|
|
|
// Stop stops an execution
|
|
func (c *ExecutionController) Stop(execID string) error
|
|
|
|
// ControlledExecution represents an execution that can be controlled
|
|
type ControlledExecution struct {
|
|
ID string
|
|
MemberID string
|
|
TeamID string
|
|
Status ExecStatus
|
|
Phase Phase
|
|
StartTime time.Time
|
|
PausedAt *time.Time
|
|
// ... internal fields for context and channels
|
|
}
|
|
|
|
// IsPaused returns true if the execution is paused
|
|
func (e *ControlledExecution) IsPaused() bool
|
|
|
|
// IsCancelled returns true if the execution is cancelled
|
|
func (e *ControlledExecution) IsCancelled() bool
|
|
|
|
// WaitIfPaused blocks until the execution is resumed or cancelled
|
|
func (e *ControlledExecution) WaitIfPaused() error
|
|
|
|
// CheckCancelled checks if the execution is cancelled and returns error if so
|
|
func (e *ControlledExecution) CheckCancelled() error
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Errors
|
|
|
|
```go
|
|
// types/errors.go
|
|
package types
|
|
|
|
import "errors"
|
|
|
|
var (
|
|
// Config errors
|
|
ErrMissingIdentity = errors.New("identity.role is required")
|
|
ErrClockTimesEmpty = errors.New("clock.times is required for times mode")
|
|
ErrClockIntervalEmpty = errors.New("clock.every is required for interval mode")
|
|
ErrClockModeInvalid = errors.New("clock.mode must be times, interval, or daemon")
|
|
|
|
// Runtime errors
|
|
ErrRobotNotFound = errors.New("robot not found")
|
|
ErrRobotPaused = errors.New("robot is paused")
|
|
ErrRobotBusy = errors.New("robot has reached max concurrent executions")
|
|
ErrTriggerDisabled = errors.New("trigger type is disabled for this robot")
|
|
ErrExecutionCancelled = errors.New("execution was cancelled")
|
|
ErrExecutionTimeout = errors.New("execution timed out")
|
|
|
|
// Phase errors
|
|
ErrPhaseAgentNotFound = errors.New("phase agent not found")
|
|
ErrGoalGenFailed = errors.New("goal generation failed")
|
|
ErrTaskPlanFailed = errors.New("task planning failed")
|
|
ErrDeliveryFailed = errors.New("delivery failed")
|
|
)
|
|
```
|