yao/agent/robot/TECHNICAL.md
Max d590bd7557 Enhance Execution Management with UI Display Fields and Localization Support
- Added `Name` and `CurrentTaskName` fields to the `Execution` struct for improved UI display during execution phases.
- Implemented localization support for UI messages, allowing dynamic updates based on the execution context and user locale.
- Updated the executor to manage these fields at various phases, ensuring accurate representation of execution status.
- Enhanced OpenAPI documentation to reflect the new fields and their usage in execution responses.
- Added unit tests to validate the functionality of UI fields and localization handling.
2026-01-24 10:10:48 +08:00

86 KiB

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)
│
└── 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 ││ 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/
plan/ types/
executor/ types/, cache/, dedup/, store/, pool/, 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)

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

// ==================== 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
    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>

// 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:

// 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().

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):

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:

// ==================== 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;
  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:

// ==================== 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:

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:

import "github.com/yaoapp/yao/agent/robot/types"

2.1 Enums

// 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"   // Email via yao/messenger
    DeliveryWebhook DeliveryType = "webhook" // POST to external URL
    DeliveryProcess DeliveryType = "process" // Yao Process call
    DeliveryNotify  DeliveryType = "notify"  // In-app notification (future)
)

// 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

// 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

// 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      *DeliveryPreferences `json:"delivery,omitempty"` // see section 6.2
    Events        []Event              `json:"events,omitempty"`
    DefaultLocale string               `json:"default_locale,omitempty"` // Default language for clock/event triggers (e.g., "en-US", "zh-CN")
}

// Validate validates the config
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
}

// Note: Delivery preferences moved to DeliveryPreferences (see section 6.2)

// 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

// 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 (stored in ExecutionStore)
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"`
    RobotEmail     string      `json:"robot_email"` // Robot's email address for sending emails

    // 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, stored in ExecutionStore
type Execution struct {
    ID          string      `json:"id"`           // unique execution ID
    MemberID    string      `json:"member_id"`    // robot member ID (globally unique)
    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"`

    // UI display fields (updated by executor at each phase)
    // These provide human-readable status for frontend display
    Name            string `json:"name,omitempty"`             // Execution title (updated when goals complete)
    CurrentTaskName string `json:"current_task_name,omitempty"` // Current task description (updated during run phase)

    // Trigger input (stored for traceability)
    Input *TriggerInput `json:"input,omitempty"` // original trigger input

    // 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
    Locale   string             `json:"locale,omitempty"`   // language for UI display (e.g., "en-US", "zh-CN")

    // For event trigger
    Source    EventSource            `json:"source,omitempty"`     // webhook | database
    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 by P4)
// Note: This is a hint from P1 Goals. Actual delivery is handled by Delivery Center
// based on Robot/User preferences, not strictly by this target.
type DeliveryTarget struct {
    Type       DeliveryType           `json:"type"`                 // Preferred delivery type
    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 validation result with multi-turn conversation support
type ValidationResult struct {
    // Basic validation result
    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 (markdown)

    // Execution state (for multi-turn conversation control)
    Complete     bool   `json:"complete"`                // whether expected result is obtained
    NeedReply    bool   `json:"need_reply,omitempty"`    // whether to continue conversation
    ReplyContent string `json:"reply_content,omitempty"` // content for next turn (if NeedReply)
}

// DeliveryRequest - pushed to Delivery Center
// Agent only generates content, Delivery Center decides channels based on preferences
type DeliveryRequest struct {
    Content *DeliveryContent `json:"content"` // Agent-generated content
    Context *DeliveryContext `json:"context"` // Tracking info
    // No Channels field - Delivery Center decides based on Robot/User preferences
}

// DeliveryContent - content generated by Delivery Agent
type DeliveryContent struct {
    Summary     string               `json:"summary"`               // Brief summary (1-2 sentences)
    Body        string               `json:"body"`                  // Full markdown report
    Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts
}

// DeliveryAttachment - task output attachment with metadata
// File uses wrapper format: __<uploader>://<fileID>
// Example: __yao.attachment://ccd472d11feb96e03a3fc468f494045c
// Parse with attachment.Parse(value) → (uploader, fileID, isWrapper)
type DeliveryAttachment struct {
    Title       string `json:"title"`                 // Human-readable title, e.g., "Market Analysis Report"
    Description string `json:"description,omitempty"` // Description of what this artifact is
    TaskID      string `json:"task_id,omitempty"`     // Which task produced this artifact
    File        string `json:"file"`                  // Wrapper format: __<uploader>://<fileID>
}

// DeliveryContext - tracking and audit info
type DeliveryContext struct {
    MemberID    string      `json:"member_id"`    // Robot member ID (globally unique)
    ExecutionID string      `json:"execution_id"`
    TriggerType TriggerType `json:"trigger_type"`
    TeamID      string      `json:"team_id"`
}

// DeliveryPreferences - Robot/User delivery preferences (read by Delivery Center)
// Each channel supports multiple targets
type DeliveryPreferences struct {
    Email   *EmailPreference   `json:"email,omitempty"`
    Webhook *WebhookPreference `json:"webhook,omitempty"`
    Process *ProcessPreference `json:"process,omitempty"`
    // notify is handled automatically based on user subscriptions
}

// EmailPreference - multiple email targets
type EmailPreference struct {
    Enabled bool          `json:"enabled"`
    Targets []EmailTarget `json:"targets"`
}

type EmailTarget struct {
    To       []string `json:"to"`                 // Recipient addresses
    Template string   `json:"template,omitempty"` // Email template ID
    Subject  string   `json:"subject,omitempty"`  // Subject template (default: content.Summary)
}

// WebhookPreference - multiple webhook targets
type WebhookPreference struct {
    Enabled bool            `json:"enabled"`
    Targets []WebhookTarget `json:"targets"`
}

type WebhookTarget struct {
    URL     string            `json:"url"`               // Webhook URL
    Method  string            `json:"method,omitempty"`  // HTTP method (default: POST)
    Headers map[string]string `json:"headers,omitempty"` // Custom headers
    Secret  string            `json:"secret,omitempty"`  // Signing secret
}

// ProcessPreference - multiple Yao Process targets
type ProcessPreference struct {
    Enabled bool            `json:"enabled"`
    Targets []ProcessTarget `json:"targets"`
}

type ProcessTarget struct {
    Process string `json:"process"`        // Yao Process name, e.g., "orders.UpdateStatus"
    Args    []any  `json:"args,omitempty"` // Additional args (DeliveryContent passed as first arg)
}

// DeliveryResult - P4 delivery output (returned by Delivery Center)
type DeliveryResult struct {
    RequestID string           `json:"request_id"`          // Delivery request ID
    Content   *DeliveryContent `json:"content"`             // Agent-generated content
    Results   []ChannelResult  `json:"results,omitempty"`   // Results per channel
    Success   bool             `json:"success"`             // Overall success
    Error     string           `json:"error,omitempty"`     // Error if failed
    SentAt    *time.Time       `json:"sent_at,omitempty"`   // When delivery completed
}

// ChannelResult - result for a single delivery target
type ChannelResult struct {
    Type       DeliveryType `json:"type"`                 // email | webhook | process
    Target     string       `json:"target"`               // Target identifier (email, URL, process name)
    Success    bool         `json:"success"`              // Whether delivery succeeded
    Recipients []string     `json:"recipients,omitempty"` // Who received (for email)
    Details    interface{}  `json:"details,omitempty"`    // Channel-specific response
    Error      string       `json:"error,omitempty"`      // Error message if failed
    SentAt     *time.Time   `json:"sent_at,omitempty"`    // When this target was delivered
}

// 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

// 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

// 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

// 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

// 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.

// 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
// 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
// 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

// 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")
)

5. P3 Implementation Details

5.1 Multi-Turn Conversation Flow

For assistant tasks, P3 uses a validator-driven multi-turn conversation:

┌──────────────────────────────────────────────────────────────┐
│                   executeAssistantWithMultiTurn              │
├──────────────────────────────────────────────────────────────┤
│  1. Create Conversation (single instance for entire task)    │
│  2. Build initial messages with task context                 │
│                                                              │
│  ┌─────────────────── Turn Loop ───────────────────────────┐ │
│  │  Phase 1: Call assistant via conv.Turn()                │ │
│  │  Phase 2: ValidateWithContext() determines:             │ │
│  │           - Complete: task done?                        │ │
│  │           - NeedReply: continue conversation?           │ │
│  │           - ReplyContent: what to send next?            │ │
│  │  Phase 3: If NeedReply, use ReplyContent as next input  │ │
│  │  Break if: Complete && Passed, or !NeedReply            │ │
│  └──────────────────────────────────────────────────────────┘ │
│                                                              │
│  3. Return output, validation, error                         │
└──────────────────────────────────────────────────────────────┘

Key points:

  • ValidateWithContext() returns NeedReply and ReplyContent
  • Conversation continues until Complete && Passed or !NeedReply
  • Max turns controlled by RunConfig.MaxTurnsPerTask

5.2 Validation Rules Format

Validation rules support two formats:

  1. Natural language: "output must be valid JSON", "must contain 'field'"
  2. Structured JSON: {"type": "type", "path": "field", "value": "array"}

Examples:

// Natural language rules (converted to semantic validation)
"output must be valid JSON"
"must contain product name"

// Structured JSON assertions
{"type": "equals", "value": "success"}
{"type": "contains", "value": "total"}
{"type": "regex", "value": "^[A-Z].*"}
{"type": "json_path", "path": "data.items", "value": 10}
{"type": "type", "path": "result", "value": "object"}

5.3 Task Dependencies

Task dependencies are handled automatically:

  1. BuildTaskContext() collects previous task results
  2. FormatPreviousResultsAsContext() formats them for assistant
// Previous results are passed as context
func (r *Runner) BuildTaskContext(exec *robottypes.Execution, taskIndex int) *RunnerContext {
    ctx := &RunnerContext{}
    if taskIndex > 0 {
        ctx.PreviousResults = exec.Results[:taskIndex]
    }
    return ctx
}

5.4 Resource Management

Agent context is properly released to prevent resource leaks:

func (c *AgentCaller) Call(ctx *robottypes.Context, assistantID string, messages []agentcontext.Message) (*CallResult, error) {
    agentCtx := c.buildAgentContext(ctx)
    defer agentCtx.Release() // IMPORTANT: Release agent context
    
    response, err := ast.Stream(agentCtx, messages, opts)
    // ...
}

5.5 yao/assert Package

The yao/assert package is a standalone universal assertion library that can be used by other modules:

import "github.com/yaoapp/yao/assert"

// Create asserter with optional callbacks
asserter := assert.NewAsserter(assert.AssertionOptions{
    AgentValidator:  myAgentValidator,  // for "agent" type assertions
    ScriptRunner:    myScriptRunner,    // for "script" type assertions
})

// Run assertions
results := asserter.Assert(output, []assert.Assertion{
    {Type: "type", Value: "object"},
    {Type: "contains", Value: "success"},
    {Type: "json_path", Path: "data.count", Value: 10},
})

Supported assertion types:

  • equals - exact match
  • contains - substring check
  • not_contains - negative substring check
  • json_path - JSON path extraction and comparison
  • regex - regex pattern matching
  • type - type checking (with optional path)
  • script - custom script validation
  • agent - AI agent validation

6. P4 Delivery Implementation

6.1 Overview

P4 Delivery summarizes P3 execution results and delivers to configured channels.

┌─────────────────────────────────────────────────────────────┐
│                      delivery.go (P4 Entry)                  │
│  - DeliveryExecution: main entry point                       │
│  - Calls Delivery Agent with full execution context          │
│  - Routes DeliveryContent to configured channels             │
└─────────────────────┬───────────────────────────────────────┘
                      │
         ┌────────────┴────────────┐
         ▼                         ▼
┌─────────────────┐      ┌─────────────────┐
│ Delivery Agent  │      │ Delivery Center  │
│  - Summarize    │      │  - sendEmail()   │
│  - Format body  │      │  - postWebhook() │
│  - List files   │      │  - callProcess() │
└─────────────────┘      └─────────────────┘

6.2 Delivery Request Structure

P4 generates a DeliveryRequest with only content and pushes to Delivery Center. Delivery Center decides channels based on Robot/User preferences.

// DeliveryRequest - pushed to Delivery Center
// No Channels - Delivery Center decides based on preferences
type DeliveryRequest struct {
    Content *DeliveryContent `json:"content"` // Agent-generated content
    Context *DeliveryContext `json:"context"` // Tracking info
}

// DeliveryContent - content generated by Delivery Agent
type DeliveryContent struct {
    Summary     string               `json:"summary"`               // Brief 1-2 sentence summary
    Body        string               `json:"body"`                  // Full markdown report
    Attachments []DeliveryAttachment `json:"attachments,omitempty"` // Output artifacts from P3
}

// DeliveryAttachment - file attachment with metadata
type DeliveryAttachment struct {
    Title       string `json:"title"`                 // Human-readable title
    Description string `json:"description,omitempty"` // What this artifact is
    TaskID      string `json:"task_id,omitempty"`     // Which task produced this
    File        string `json:"file"`                  // Wrapper: __<uploader>://<fileID>
}

// DeliveryContext - tracking and audit info
type DeliveryContext struct {
    MemberID    string      `json:"member_id"`    // Robot member ID (globally unique)
    ExecutionID string      `json:"execution_id"`
    TriggerType TriggerType `json:"trigger_type"`
    TeamID      string      `json:"team_id"`
}

Example DeliveryRequest:

{
  "content": {
    "summary": "Sales report completed: 15 new leads",
    "body": "## Weekly Sales Report\n...",
    "attachments": [{"title": "Report.pdf", "file": "__yao.attachment://abc123"}]
  },
  "context": {
    "member_id": "mem_abc123",
    "execution_id": "exec_xyz789",
    "trigger_type": "clock",
    "team_id": "team_123"
  }
}

Channel Decision by Delivery Center:

Delivery Center reads Robot/User preferences and executes delivery to all enabled targets:

// DeliveryPreferences - from Robot config (each channel supports multiple targets)
type DeliveryPreferences struct {
    Email   *EmailPreference   `json:"email,omitempty"`
    Webhook *WebhookPreference `json:"webhook,omitempty"`
    Process *ProcessPreference `json:"process,omitempty"`
}

type EmailPreference struct {
    Enabled bool          `json:"enabled"`
    Targets []EmailTarget `json:"targets"` // Multiple email targets
}

type WebhookPreference struct {
    Enabled bool            `json:"enabled"`
    Targets []WebhookTarget `json:"targets"` // Multiple webhook URLs
}

type ProcessPreference struct {
    Enabled bool            `json:"enabled"`
    Targets []ProcessTarget `json:"targets"` // Multiple Yao Process calls
}

6.3 File Wrapper Format

Attachments use the standard yao/attachment wrapper format:

// Format: __<uploader>://<fileID>
// Example: __yao.attachment://ccd472d11feb96e03a3fc468f494045c

import "github.com/yaoapp/yao/attachment"

// Parse wrapper to get uploader and fileID
uploader, fileID, isWrapper := attachment.Parse(wrapper)
// uploader: "__yao.attachment"
// fileID: "ccd472d11feb96e03a3fc468f494045c"
// isWrapper: true

// Get file info
manager := attachment.Managers[uploader]
fileInfo, err := manager.Info(ctx, fileID)

// Read file content as base64
base64Content := attachment.Base64(ctx, wrapper)

// Read with data URI format
dataURI := attachment.Base64(ctx, wrapper, true)
// "data:image/png;base64,..."

6.4 Delivery Agent

The Delivery Agent only generates content, does NOT decide channels. Channel decisions are made by Delivery Center based on Robot/User preferences.

Input:

type DeliveryAgentInput struct {
    Robot       *Robot             `json:"robot"`       // Robot identity and config
    TriggerType TriggerType        `json:"trigger"`     // clock | human | event
    Inspiration *InspirationReport `json:"inspiration"` // P0 (clock only)
    Goals       *Goals             `json:"goals"`       // P1
    Tasks       []Task             `json:"tasks"`       // P2
    Results     []TaskResult       `json:"results"`     // P3
}

Output:

// DeliveryAgentOutput - only content, no channels
type DeliveryAgentOutput struct {
    Content *DeliveryContent `json:"content"` // Generated content
}

Agent Responsibilities:

The agent focuses on content generation:

  • Summary: Brief 1-2 sentence summary of execution results
  • Body: Full markdown report with details
  • Attachments: Select which P3-generated files to include

Example Output:

{
  "content": {
    "summary": "Sales report completed: 15 new leads processed, 3 high-priority",
    "body": "## Weekly Sales Report\n\n### Summary\n- Total leads: 15\n- High priority: 3\n...",
    "attachments": [
      {"title": "Sales Report.pdf", "task_id": "task_1", "file": "__yao.attachment://abc123"},
      {"title": "Lead Analysis.xlsx", "task_id": "task_2", "file": "__yao.attachment://def456"}
    ]
  }
}

6.5 Global Email Configuration

Email delivery uses global configuration for channel selection and Robot-specific sender identity:

// types/config_global.go

// DefaultEmailChannel returns the default messenger channel name
// Default: "email" (maps to messengers/channels.yao)
func DefaultEmailChannel() string

// SetDefaultEmailChannel sets the default channel (call during agent init)
func SetDefaultEmailChannel(channel string)

Usage:

  • DefaultEmailChannel() - returns the messenger channel name for email delivery
  • Robot.RobotEmail - used as the From address when sending emails
  • If RobotEmail is empty, falls back to provider's default from address

6.6 Delivery Center

The Delivery Center receives DeliveryRequest, reads preferences, and executes delivery to all enabled targets.

Current implementation: Internal to P4 (in executor/delivery.go) Future: Can be extracted to standalone yao/delivery package

// DeliveryCenter - handles delivery execution to multiple targets
type DeliveryCenter struct {
    messenger *messenger.Manager
}

// Deliver - main entry point
func (dc *DeliveryCenter) Deliver(ctx context.Context, req *DeliveryRequest) *DeliveryResult {
    requestID := generateID()
    prefs := dc.getDeliveryPreferences(ctx, req.Context.MemberID)
    
    var results []ChannelResult
    allSuccess := true
    
    // Email - send to all targets (robot passed for From address)
    if prefs.Email != nil && prefs.Email.Enabled {
        for _, target := range prefs.Email.Targets {
            result := dc.sendEmail(ctx, req.Content, target, req.Context, robot)
            results = append(results, result)
            if !result.Success {
                allSuccess = false
            }
        }
    }
    
    // Webhook - POST to all targets
    if prefs.Webhook != nil && prefs.Webhook.Enabled {
        for _, target := range prefs.Webhook.Targets {
            result := dc.postWebhook(ctx, req.Content, target)
            results = append(results, result)
            if !result.Success {
                allSuccess = false
            }
        }
    }
    
    // Process - call all targets
    if prefs.Process != nil && prefs.Process.Enabled {
        for _, target := range prefs.Process.Targets {
            result := dc.callProcess(ctx, req.Content, target)
            results = append(results, result)
            if !result.Success {
                allSuccess = false
            }
        }
    }
    
    // Future: auto-notify based on user subscriptions
    // dc.sendNotifications(ctx, req)
    
    return &DeliveryResult{
        RequestID: requestID,
        Content:   req.Content,
        Success:   allSuccess,
        Results:   results,
    }
}

6.7 Channel Handlers

Each delivery channel is handled by dedicated methods in DeliveryCenter:

// sendEmail - send to a single email target
// Uses Robot.RobotEmail as From address and global DefaultEmailChannel()
func (dc *DeliveryCenter) sendEmail(
    ctx context.Context,
    content *DeliveryContent,
    target EmailTarget,
    deliveryCtx *DeliveryContext,
    robot *Robot,
) ChannelResult {
    // Convert attachments to messenger format
    var attachments []messenger.Attachment
    for _, att := range content.Attachments {
        uploader, fileID, _ := attachment.Parse(att.File)
        manager := attachment.Managers[uploader]
        data, _ := manager.Read(ctx, fileID)
        info, _ := manager.Info(ctx, fileID)
        
        attachments = append(attachments, messenger.Attachment{
            Filename:    att.Title,
            ContentType: info.ContentType,
            Content:     data,
        })
    }
    
    subject := content.Summary
    if target.Subject != "" {
        subject = target.Subject
    }
    
    msg := &messenger.Message{
        To:          target.To,
        Subject:     subject,
        Body:        content.Body,
        Attachments: attachments,
    }
    
    // Set From address from Robot's email (if configured)
    if robot != nil && robot.RobotEmail != "" {
        msg.From = robot.RobotEmail
    }
    
    // Use global default email channel
    channel := DefaultEmailChannel() // from types/config_global.go
    err := dc.messenger.Send(ctx, channel, msg)
    
    now := time.Now()
    return ChannelResult{
        Type:       DeliveryEmail,
        Target:     strings.Join(target.To, ","),
        Success:    err == nil,
        Recipients: target.To,
        SentAt:     &now,
        Error:      errStr(err),
    }
}

// postWebhook - POST to a single webhook target
func (dc *DeliveryCenter) postWebhook(ctx context.Context, content *DeliveryContent, target WebhookTarget) ChannelResult {
    payload, _ := json.Marshal(content)
    req, _ := http.NewRequestWithContext(ctx, "POST", target.URL, bytes.NewReader(payload))
    req.Header.Set("Content-Type", "application/json")
    
    // Add custom headers
    for k, v := range target.Headers {
        req.Header.Set(k, v)
    }
    
    resp, err := http.DefaultClient.Do(req)
    now := time.Now()
    
    if err != nil {
        return ChannelResult{
            Type:    DeliveryWebhook,
            Target:  target.URL,
            Success: false,
            Error:   err.Error(),
            SentAt:  &now,
        }
    }
    defer resp.Body.Close()
    
    success := resp.StatusCode < 400
    return ChannelResult{
        Type:    DeliveryWebhook,
        Target:  target.URL,
        Success: success,
        Details: map[string]interface{}{"status_code": resp.StatusCode},
        Error:   ternary(!success, fmt.Sprintf("HTTP %d", resp.StatusCode), ""),
        SentAt:  &now,
    }
}

// callProcess - call a single Yao Process target
func (dc *DeliveryCenter) callProcess(ctx context.Context, content *DeliveryContent, target ProcessTarget) ChannelResult {
    // DeliveryContent as first arg, then additional args
    args := append([]interface{}{content}, target.Args...)
    
    proc := process.Of(target.Process, args...)
    result, err := proc.Execute()
    
    now := time.Now()
    return ChannelResult{
        Type:    DeliveryProcess,
        Target:  target.Process,
        Success: err == nil,
        Details: map[string]interface{}{
            "process": target.Process,
            "result":  result,
        },
        Error:  errStr(err),
        SentAt: &now,
    }
}

Note on Notifications:

notify is NOT configured per-Robot. Future Delivery Center will:

  1. Check user subscription preferences after receiving DeliveryRequest
  2. Automatically send in-app notifications to subscribed users
  3. This is transparent to P4 and Delivery Agent

6.8 Execution Persistence

Robot execution history is stored in __yao.agent_execution table for UI display:

// Model: yao/models/agent/execution.mod.yao
// Table: __yao.agent_execution

type ExecutionRecord struct {
    ID          int64                  `json:"id,omitempty"`     // Auto-increment primary key
    ExecutionID string                 `json:"execution_id"`     // Unique execution identifier
    MemberID    string                 `json:"member_id"`        // Robot member ID (globally unique)
    TeamID      string                 `json:"team_id"`          // Team ID
    TriggerType TriggerType            `json:"trigger_type"`     // clock | human | event
    
    // Status tracking (synced with runtime Execution)
    Status      ExecStatus             `json:"status"`           // pending | running | completed | failed | cancelled
    Phase       Phase                  `json:"phase"`            // Current phase
    Current     *CurrentState          `json:"current,omitempty"`// Current executing state (task index, progress)
    Error       string                 `json:"error,omitempty"`  // Error message if failed
    
    // Trigger input
    Input       *TriggerInput          `json:"input,omitempty"`  // Original trigger input
    
    // Phase outputs (P0-P5)
    Inspiration *InspirationReport     `json:"inspiration,omitempty"` // P0 result
    Goals       *Goals                 `json:"goals,omitempty"`       // P1 result
    Tasks       []Task                 `json:"tasks,omitempty"`       // P2 result
    Results     []TaskResult           `json:"results,omitempty"`     // P3 results
    Delivery    *DeliveryResult        `json:"delivery,omitempty"`    // P4 result
    Learning    []LearningEntry        `json:"learning,omitempty"`    // P5 entries
    
    // Timestamps
    StartTime   *time.Time             `json:"start_time,omitempty"`
    EndTime     *time.Time             `json:"end_time,omitempty"`
    CreatedAt   *time.Time             `json:"created_at,omitempty"`
    UpdatedAt   *time.Time             `json:"updated_at,omitempty"`
}

// CurrentState - current executing state (for JSON storage)
type CurrentState struct {
    TaskIndex int    `json:"task_index"`         // index in Tasks slice
    Progress  string `json:"progress,omitempty"` // human-readable progress (e.g., "2/5 tasks")
}

Store Implementation:

// store/execution.go
type ExecutionStore struct {
    modelID string // "__yao.agent.execution"
}

func NewExecutionStore() *ExecutionStore

// Save creates or updates an execution record
func (s *ExecutionStore) Save(ctx context.Context, record *ExecutionRecord) error

// Get retrieves an execution by execution_id
func (s *ExecutionStore) Get(ctx context.Context, executionID string) (*ExecutionRecord, error)

// List retrieves executions with filters
func (s *ExecutionStore) List(ctx context.Context, opts *ListOptions) ([]*ExecutionRecord, error)

// UpdatePhase updates the current phase and its data
func (s *ExecutionStore) UpdatePhase(ctx context.Context, executionID string, phase Phase, data interface{}) error

// UpdateStatus updates the execution status
func (s *ExecutionStore) UpdateStatus(ctx context.Context, executionID string, status ExecStatus, errorMsg string) error

// UpdateCurrent updates the current executing state
func (s *ExecutionStore) UpdateCurrent(ctx context.Context, executionID string, current *CurrentState) error

// Delete removes an execution record
func (s *ExecutionStore) Delete(ctx context.Context, executionID string) error

// Conversion helpers
func FromExecution(exec *Execution) *ExecutionRecord
func (r *ExecutionRecord) ToExecution() *Execution

type ListOptions struct {
    MemberID    string       // Filter by robot member ID (globally unique)
    TeamID      string       // Filter by team
    Status      ExecStatus   // Filter by status
    TriggerType TriggerType  // Filter by trigger
    Limit       int          // Max records to return (default: 100)
    Offset      int          // Skip records for pagination
    OrderBy     string       // e.g., "start_time desc"
}