Skip to main content

Overview

The Secretary Agent implements a human-in-the-loop workflow pattern with five phases: receiving ideas, clarifying requirements, dispatching tasks, monitoring execution, and generating reports. It provides an extensible framework for intelligent assistants with customizable behaviors.

Architecture

┌─────────────────────────────────────────────────────────┐
│              Secretary Agent Workflow                   │
├─────────────────────────────────────────────────────────┤
│  1. Receive Ideas     → Record todos                    │
│  2. Clarify Requirements → Project documents            │
│  3. Dispatch & Schedule → Call execution agents         │
│  4. Monitor Feedback  → Push key decisions to humans    │
│  5. Generate Report   → Update todos, deliverables      │
└─────────────────────────────────────────────────────────┘

Core Engine

SecretaryCore

Lightweight event loop engine that manages the secretary lifecycle.
pub struct SecretaryCore<B: SecretaryBehavior> {
    behavior: B,
    config: SecretaryCoreConfig,
}
start
async fn
Starts the secretary as an async task
pub async fn start<C>(
    self,
    connection: C,
) -> (SecretaryHandle, JoinHandle<GlobalResult<()>>)
where
    C: UserConnection<Input = B::Input, Output = B::Output> + 'static
Returns:
  • SecretaryHandle: Control handle for managing the secretary
  • JoinHandle: Task handle for the event loop
run
async fn
Runs the secretary synchronously (blocking)
pub async fn run<C>(self, connection: C) -> GlobalResult<()>
where
    C: UserConnection<Input = B::Input, Output = B::Output> + 'static

SecretaryCoreBuilder

Builder for configuring secretary core behavior.
let core = SecretaryCoreBuilder::new(behavior)
    .with_poll_interval(100)
    .with_welcome(true)
    .with_periodic_check(true)
    .with_periodic_check_interval(1000)
    .with_max_consecutive_errors(10)
    .build();
with_poll_interval
u64
default:"100"
Event loop polling interval in milliseconds
with_welcome
bool
default:"true"
Send welcome message on startup
with_periodic_check
bool
default:"true"
Enable periodic background checks
with_periodic_check_interval
u64
default:"1000"
Interval for periodic checks in milliseconds
with_max_consecutive_errors
u32
default:"10"
Maximum consecutive errors before stopping

SecretaryHandle

Control handle for managing secretary state.
// Check status
if handle.is_running() {
    println!("Secretary is active");
}

// Pause/Resume
handle.pause();
handle.resume();

// Stop
handle.stop().await;
is_running
fn() -> bool
Check if secretary is running
is_paused
fn() -> bool
Check if secretary is paused
pause
fn()
Pause the secretary (stops processing input)
resume
fn()
Resume the secretary
stop
async fn()
Stop the secretary gracefully

Behavior Trait

SecretaryBehavior

Core trait defining secretary behavior (from mofa-kernel).
#[async_trait]
pub trait SecretaryBehavior: Send + Sync {
    type Input: SecretaryInput;
    type Output: SecretaryOutput;
    type State: Send + Sync;

    async fn handle_input(
        &self,
        input: Self::Input,
        ctx: &mut SecretaryContext<Self::State>,
    ) -> GlobalResult<Vec<Self::Output>>;

    fn initial_state(&self) -> Self::State;
    
    fn welcome_message(&self) -> Option<Self::Output> { None }
    
    async fn periodic_check(
        &self,
        ctx: &mut SecretaryContext<Self::State>,
    ) -> GlobalResult<Vec<Self::Output>> { Ok(vec![]) }
    
    fn handle_error(&self, error: &GlobalError) -> Option<Self::Output> { None }
    
    async fn on_disconnect(
        &self,
        ctx: &mut SecretaryContext<Self::State>,
    ) -> GlobalResult<()> { Ok(()) }
}
Input
type
required
Input message type (must implement SecretaryInput)
Output
type
required
Output message type (must implement SecretaryOutput)
State
type
required
Internal state type (must be Send + Sync)
handle_input
async fn
required
Process user input and return outputs
initial_state
fn
required
Create initial state
welcome_message
fn
Optional welcome message sent on startup
periodic_check
async fn
Background task executed periodically
handle_error
fn
Convert errors to user-facing output
on_disconnect
async fn
Cleanup on connection close

Default Implementation

DefaultSecretaryBehavior

Complete secretary implementation with todo management, clarification, dispatch, and monitoring.
let behavior = DefaultSecretaryBuilder::new()
    .with_name("Project Secretary")
    .with_dispatch_strategy(DispatchStrategy::CapabilityFirst)
    .with_auto_clarify(true)
    .with_auto_dispatch(true)
    .with_llm(llm_provider)
    .with_executor(frontend_agent)
    .with_executor(backend_agent)
    .build();

DefaultSecretaryBuilder

Builder for default secretary behavior.
with_name
String
Secretary display name
with_dispatch_strategy
DispatchStrategy
Strategy for assigning tasks:
  • CapabilityFirst: Match by agent capabilities
  • LoadBalancing: Distribute evenly
  • RoundRobin: Rotate assignments
  • Random: Random selection
with_auto_clarify
bool
default:"false"
Automatically clarify ambiguous requirements
with_auto_dispatch
bool
default:"false"
Automatically dispatch to execution agents
with_llm
Arc<dyn LLMProvider>
LLM provider for intelligent processing
with_executor
AgentInfo
Register an execution agent

Input/Output Types

DefaultInput

Standard input messages for secretary.
Idea
User submits an idea or request
DefaultInput::Idea {
    content: String,
    priority: Option<TodoPriority>,
    metadata: Option<HashMap<String, String>>,
}
Query
Query secretary status or information
DefaultInput::Query {
    query_type: QueryType,
    parameters: Option<HashMap<String, String>>,
}
Command
Execute secretary command
DefaultInput::Command {
    command: SecretaryCommand,
    args: Vec<String>,
}
Response
Human response to secretary question
DefaultInput::Response {
    response: HumanResponse,
}

DefaultOutput

Standard output messages from secretary.
Message
General message to user
DefaultOutput::Message { content: String }
Acknowledgment
Confirm receipt and processing
DefaultOutput::Acknowledgment { message: String }
DecisionRequired
Request human decision
DefaultOutput::DecisionRequired {
    decision: CriticalDecision,
}
Report
Status or completion report
DefaultOutput::Report { report: Report }
Error
Error notification
DefaultOutput::Error { message: String }

Components

TodoManager

Manages todo items with priorities and status tracking.
let mut manager = TodoManager::new();

// Add todo
let id = manager.add_todo(
    "Implement feature",
    Some(TodoPriority::High),
    None,
)?;

// Update status
manager.update_status(&id, TodoStatus::InProgress)?;

// Query
let pending = manager.get_by_status(TodoStatus::Pending);
let high_priority = manager.get_by_priority(TodoPriority::High);
add_todo
fn
Add new todo item
pub fn add_todo(
    &mut self,
    content: impl Into<String>,
    priority: Option<TodoPriority>,
    metadata: Option<HashMap<String, String>>,
) -> GlobalResult<String>
update_status
fn
Update todo status
pub fn update_status(
    &mut self,
    id: &str,
    status: TodoStatus,
) -> GlobalResult<()>
get_by_status
fn
Query todos by status
pub fn get_by_status(&self, status: TodoStatus) -> Vec<&TodoItem>

RequirementClarifier

Clarifies ambiguous requirements through LLM analysis.
let clarifier = RequirementClarifier::new(llm_provider);

let questions = clarifier.clarify_requirement(
    "Build a web app",
    ClarificationStrategy::Interactive,
).await?;
strategy
ClarificationStrategy
Clarification approach:
  • Interactive: Ask user questions
  • Automatic: LLM infers requirements
  • Hybrid: Combine both approaches

TaskCoordinator

Coordinates task dispatch to execution agents.
let coordinator = TaskCoordinator::new(
    agent_router,
    DispatchStrategy::CapabilityFirst,
);

let result = coordinator.dispatch_task(
    "Implement UI",
    vec!["frontend", "ui_design"],
).await?;
dispatch_task
async fn
Dispatch task to appropriate agent
pub async fn dispatch_task(
    &self,
    task: &str,
    required_capabilities: Vec<&str>,
) -> GlobalResult<DispatchResult>

TaskMonitor

Monitors task execution with rule-based alerts.
let monitor = TaskMonitor::new();

// Add monitoring rule
monitor.add_rule(MonitoringRule {
    name: "timeout_alert",
    condition: |snapshot| snapshot.elapsed_ms > 60000,
    action: |snapshot| {
        // Send alert
    },
});

// Update task
monitor.update_task(&task_id, snapshot).await?;

Reporter

Generates reports in various formats.
let reporter = Reporter::new();

let report = reporter.generate(
    ReportType::Progress,
    ReportConfig {
        format: ReportFormat::Markdown,
        include_details: true,
        include_timeline: true,
    },
).await?;

Connection Types

ChannelConnection

In-memory channel-based connection.
let (connection, input_tx, output_rx) = ChannelConnection::new_pair(32);

// Send input
input_tx.send(DefaultInput::Idea {
    content: "New feature".to_string(),
    priority: None,
    metadata: None,
}).await?;

// Receive output
while let Some(output) = output_rx.recv().await {
    match output {
        DefaultOutput::Message { content } => {
            println!("Secretary: {}", content);
        }
        _ => {}
    }
}

TimeoutConnection

Wrapper adding timeout behavior to connections.
let connection = TimeoutConnection::new(
    base_connection,
    Duration::from_secs(30),
);

Agent Routing

AgentRouter

Routes tasks to appropriate execution agents.
let router = AgentRouter::new(agent_provider)
    .with_capability_matching(true)
    .with_load_balancing(true);

let agent = router.route(
    &task,
    &["frontend", "react"],
).await?;

AgentInfo

Metadata about execution agents.
let mut agent = AgentInfo::new("frontend_agent", "Frontend Development Agent");
agent.capabilities = vec!["frontend", "ui_design", "react"];
agent.current_load = 20;
agent.available = true;
agent.performance_score = 0.85;
id
String
required
Unique agent identifier
name
String
required
Human-readable name
capabilities
Vec<String>
Agent capabilities/skills
current_load
u32
Current workload (0-100)
available
bool
Whether agent is available
performance_score
f32
Performance rating (0.0-1.0)

LLM Integration

LLMProvider Trait

Trait for LLM integration in secretary.
#[async_trait]
pub trait LLMProvider: Send + Sync {
    fn name(&self) -> &str;
    async fn chat(&self, messages: &[ChatMessage]) -> GlobalResult<String>;
    async fn analyze(&self, prompt: &str) -> GlobalResult<serde_json::Value>;
}

ChatMessage

Conversation message for LLM.
let messages = vec![
    ChatMessage::system("You are a helpful assistant"),
    ChatMessage::user("Clarify this requirement: Build a web app"),
];

let response = llm_provider.chat(&messages).await?;

Work Phases

WorkPhase

Secretary workflow phases.
Receiving
Receiving and recording ideas
Clarifying
Clarifying requirements
Dispatching
Dispatching to execution agents
Monitoring
Monitoring task progress
Reporting
Generating reports

Complete Example

use mofa_foundation::secretary::*;
use mofa_foundation::llm::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Create LLM provider
    let llm_provider = create_openai_provider(api_key);
    
    // Define execution agents
    let mut frontend_agent = AgentInfo::new(
        "frontend_agent",
        "Frontend Developer"
    );
    frontend_agent.capabilities = vec!["frontend", "react", "ui"];
    
    let mut backend_agent = AgentInfo::new(
        "backend_agent",
        "Backend Developer"
    );
    backend_agent.capabilities = vec!["backend", "api", "database"];
    
    // Build secretary
    let secretary = DefaultSecretaryBuilder::new()
        .with_name("Development Secretary")
        .with_dispatch_strategy(DispatchStrategy::CapabilityFirst)
        .with_auto_clarify(true)
        .with_auto_dispatch(true)
        .with_llm(llm_provider)
        .with_executor(frontend_agent)
        .with_executor(backend_agent)
        .build();
    
    // Create connection
    let (connection, input_tx, mut output_rx) = 
        ChannelConnection::new_pair(32);
    
    // Start secretary
    let (handle, join_handle) = SecretaryCore::new(secretary)
        .start(connection)
        .await;
    
    // Send idea
    input_tx.send(DefaultInput::Idea {
        content: "Build a user management system".to_string(),
        priority: Some(TodoPriority::High),
        metadata: None,
    }).await?;
    
    // Receive responses
    while let Some(output) = output_rx.recv().await {
        match output {
            DefaultOutput::Message { content } => {
                println!("Secretary: {}", content);
            }
            DefaultOutput::DecisionRequired { decision } => {
                println!("Decision needed: {}", decision.description);
                // Send human response
            }
            DefaultOutput::Report { report } => {
                println!("Report:\n{}", report.content);
            }
            _ => {}
        }
    }
    
    // Stop secretary
    handle.stop().await;
    join_handle.await??;
    
    Ok(())
}

Source Reference

  • Core engine: ~/workspace/source/crates/mofa-foundation/src/secretary/core.rs
  • Module exports: ~/workspace/source/crates/mofa-foundation/src/secretary/mod.rs
  • Default implementation: ~/workspace/source/crates/mofa-foundation/src/secretary/default/
  • Example: ~/workspace/source/examples/secretary_agent/src/main.rs

Build docs developers (and LLMs) love