Skip to main content

Overview

The Secretary Agent pattern implements a human-in-the-loop workflow system where an AI secretary manages tasks, clarifies requirements, dispatches work to execution agents, and escalates key decisions to humans.

What You’ll Learn

  • Building secretary agents for workflow management
  • Registering and managing execution agents
  • Automatic task clarification and dispatch
  • Human decision escalation patterns
  • LLM integration for intelligent coordination

Prerequisites

  • Rust 1.75 or higher
  • OpenAI API key (for LLM integration)
  • Understanding of async channels

Architecture

Source Code

use mofa_sdk::secretary::{
    AgentInfo, ChannelConnection, DefaultInput, DefaultOutput,
    DefaultSecretaryBuilder, DispatchStrategy, SecretaryCore
};
use tracing::info;

mod llm_integration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    tracing_subscriber::fmt()
        .with_env_filter("info")
        .init();

    info!("╔══════════════════════════════════════════════════════════════╗");
    info!("║   Secretary Agent Mode - LLM-based Work Cycle Demonstration  ║");
    info!("╚══════════════════════════════════════════════════════════════╝\n");

    // Create LLM provider
    let llm_provider = llm_integration::create_llm_provider();
    info!("✅ LLM provider created: {}\n", llm_provider.name());

    // Create execution agent information
    let mut frontend_agent = AgentInfo::new(
        "frontend_agent", 
        "Frontend Development Agent"
    );
    frontend_agent.capabilities = vec![
        "frontend".to_string(),
        "ui_design".to_string(),
        "react".to_string(),
    ];
    frontend_agent.current_load = 20;
    frontend_agent.available = true;
    frontend_agent.performance_score = 0.85;

    let mut backend_agent = AgentInfo::new(
        "backend_agent", 
        "Backend Development Agent"
    );
    backend_agent.capabilities = vec![
        "backend".to_string(),
        "api_design".to_string(),
        "database".to_string(),
    ];
    backend_agent.current_load = 30;
    backend_agent.available = true;
    backend_agent.performance_score = 0.9;

    let mut test_agent = AgentInfo::new("test_agent", "Testing Agent");
    test_agent.capabilities = vec!["testing".to_string(), "qa".to_string()];
    test_agent.current_load = 10;
    test_agent.available = true;
    test_agent.performance_score = 0.88;

    // Create secretary agent
    let secretary_behavior = DefaultSecretaryBuilder::new()
        .with_name("LLM-based Development Project Secretary")
        .with_dispatch_strategy(DispatchStrategy::CapabilityFirst)
        .with_auto_clarify(true)   // Auto clarification
        .with_auto_dispatch(true)  // Auto dispatch
        .with_llm(llm_provider)
        .with_executor(frontend_agent)
        .with_executor(backend_agent)
        .with_executor(test_agent)
        .build();

    info!("✅ Secretary Agent created\n");

    // Create channel connection
    let (connection, input_tx, mut output_rx) = 
        ChannelConnection::new_pair(32);

    // Create and start core engine
    let (handle, join_handle) = SecretaryCore::new(secretary_behavior)
        .start(connection)
        .await;

    info!("✅ Secretary Agent core engine started\n");

    // Send test idea
    info!("📥 Sending idea to Secretary Agent:");
    info!("   'Develop a user management system...'");

    let idea = DefaultInput::Idea {
        content: "Develop a user management system with \
                  registration, login, and role-based access control"
            .to_string(),
        priority: None,
        metadata: None,
    };

    input_tx.send(idea).await?;

    // Receive responses
    info!("📤 Receiving Secretary Agent response:\n");

    let mut interval = tokio::time::interval(
        tokio::time::Duration::from_millis(100)
    );
    let timeout = tokio::time::sleep(
        tokio::time::Duration::from_secs(10)
    );
    tokio::pin!(timeout);

    let mut received_welcome = false;
    let mut received_response = false;

    loop {
        tokio::select! {
            _ = &mut timeout => {
                info!("   ⏰ Timeout reached");
                break;
            },
            _ = interval.tick() => {
                match output_rx.try_recv() {
                    Ok(result) => {
                        match result {
                            DefaultOutput::Message { content } => {
                                if content.contains("Welcome") {
                                    info!("   💬 Welcome: {}", content);
                                    received_welcome = true;
                                } else {
                                    info!("   💬 Message: {}", content);
                                }
                            },
                            DefaultOutput::Acknowledgment { message } => {
                                info!("   ✅ Ack: {}", message);
                                received_response = true;
                            },
                            DefaultOutput::DecisionRequired { decision } => {
                                info!("   ⏸️  Decision: {}", 
                                    decision.description);
                                received_response = true;
                            },
                            _ => {}
                        }
                    },
                    Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
                        break;
                    },
                    _ => {}
                }

                if received_welcome && received_response {
                    break;
                }
            }
        }
    }

    // Stop agent
    handle.stop().await;
    join_handle.abort();

    info!("\n✅ Example completed!");
    Ok(())
}

Running the Example

1
Set Environment Variables
2
export OPENAI_API_KEY="your-api-key"
3
Run the Example
4
cd examples/secretary_agent
cargo run

Expected Output

╔══════════════════════════════════════════════════════════════╗
║   Secretary Agent Mode - LLM-based Work Cycle Demonstration  ║
╚══════════════════════════════════════════════════════════════╝

✅ LLM provider created: OpenAIProvider

✅ Secretary Agent created

✅ Secretary Agent core engine started

📥 Sending idea to Secretary Agent:
   'Develop a user management system...'

📤 Receiving Secretary Agent response:

   💬 Welcome: Hello! I'm your project secretary...
   ✅ Ack: I've recorded your idea and will create a project plan
   ⏸️  Decision: Should we prioritize security or ease of use?

✅ Example completed!

Secretary Workflow Phases

1
Phase 1: Receive Ideas
2
Secretary records todos and ideas from the user.
3
let idea = DefaultInput::Idea {
    content: "Build a new feature".to_string(),
    priority: Some(Priority::High),
    metadata: None,
};
4
Phase 2: Clarify Requirements
5
Secretary uses LLM to ask clarifying questions and create project documents.
6
.with_auto_clarify(true)  // Enable automatic clarification
7
Phase 3: Schedule & Dispatch
8
Secretary assigns tasks to execution agents based on capabilities.
9
.with_dispatch_strategy(DispatchStrategy::CapabilityFirst)
10
Phase 4: Monitor Feedback
11
Secretary tracks progress and escalates key decisions to humans.
12
DefaultOutput::DecisionRequired { decision }
13
Phase 5: Acceptance Report
14
Secretary updates todos and provides completion reports.
15
DefaultOutput::Report { report }

Dispatch Strategies

Match tasks to agents based on capabilities:
.with_dispatch_strategy(DispatchStrategy::CapabilityFirst)
Best for: Task-agent matching by skills

Agent Configuration

Execution Agent Setup

let mut agent = AgentInfo::new("agent_id", "Agent Name");

// Set capabilities
agent.capabilities = vec![
    "skill1".to_string(),
    "skill2".to_string(),
];

// Set availability
agent.available = true;

// Set current workload (0-100)
agent.current_load = 25;

// Set performance score (0.0-1.0)
agent.performance_score = 0.85;

Secretary Builder Options

let secretary = DefaultSecretaryBuilder::new()
    .with_name("My Secretary")                // Secretary name
    .with_llm(llm_provider)                    // LLM for intelligence
    .with_auto_clarify(true)                   // Auto clarification
    .with_auto_dispatch(true)                  // Auto task dispatch
    .with_dispatch_strategy(strategy)          // Routing strategy
    .with_executor(agent1)                     // Register agent 1
    .with_executor(agent2)                     // Register agent 2
    .with_max_clarifications(3)                // Max clarify rounds
    .with_decision_threshold(0.7)              // Human escalation threshold
    .build();

Input Types

Submit new ideas or tasks:
DefaultInput::Idea {
    content: "Build feature X".to_string(),
    priority: Some(Priority::High),
    metadata: Some(metadata),
}

Output Types

General messages from secretary:
DefaultOutput::Message { content }
Task receipt confirmation:
DefaultOutput::Acknowledgment { message }
Clarifying question:
DefaultOutput::Question { question }
Escalated decision:
DefaultOutput::DecisionRequired { decision }
Status or completion report:
DefaultOutput::Report { report }
Error message:
DefaultOutput::Error { message }

Common Use Cases

Project Management

Coordinate development tasks across teams

Customer Support

Triage and route support tickets

Content Pipeline

Manage content creation workflows

DevOps Automation

Coordinate deployment and operations

Advanced Features

Custom Behaviors

Implement your own secretary behavior:
use mofa_sdk::secretary::SecretaryBehavior;

struct CustomSecretary {
    // Your fields
}

#[async_trait]
impl SecretaryBehavior for CustomSecretary {
    async fn handle_idea(&mut self, idea: Idea) -> Result<Vec<Output>> {
        // Custom idea handling
    }
    
    async fn handle_clarification(&mut self, clarif: Clarification) 
        -> Result<Vec<Output>> {
        // Custom clarification logic
    }
    
    // Implement other methods...
}

State Persistence

Persist secretary state:
let secretary = secretary_builder
    .with_persistence(Box::new(PostgresStore::new(pool)))
    .build();

Troubleshooting

Problem: All agents busy or unavailableSolution: Set agent availability or add more agents:
agent.available = true;
agent.current_load = 50;  // Not at max capacity
Problem: Expected human decision but auto-handledSolution: Lower decision threshold:
.with_decision_threshold(0.5)  // Lower = more escalations
Problem: Communication channel closedSolution: Check channel buffer size:
ChannelConnection::new_pair(128)  // Larger buffer

Next Steps

Workflow Orchestration

Build complex orchestration workflows

Multi-Agent

Coordinate multiple agent types

Secretary Guide

Deep dive into secretary pattern

HITL Patterns

Human-in-the-loop best practices

Build docs developers (and LLMs) love