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,
}
Starts the secretary as an async taskpub 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
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();
Event loop polling interval in milliseconds
Send welcome message on startup
Enable periodic background checks
with_periodic_check_interval
Interval for periodic checks in milliseconds
with_max_consecutive_errors
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;
Check if secretary is running
Check if secretary is paused
Pause the secretary (stops processing input)
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 message type (must implement SecretaryInput)
Output message type (must implement SecretaryOutput)
Internal state type (must be Send + Sync)
Process user input and return outputs
Optional welcome message sent on startup
Background task executed periodically
Convert errors to user-facing output
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.
Strategy for assigning tasks:
CapabilityFirst: Match by agent capabilities
LoadBalancing: Distribute evenly
RoundRobin: Rotate assignments
Random: Random selection
Automatically clarify ambiguous requirements
Automatically dispatch to execution agents
LLM provider for intelligent processing
Register an execution agent
Standard input messages for secretary.
User submits an idea or requestDefaultInput::Idea {
content: String,
priority: Option<TodoPriority>,
metadata: Option<HashMap<String, String>>,
}
Query secretary status or informationDefaultInput::Query {
query_type: QueryType,
parameters: Option<HashMap<String, String>>,
}
Execute secretary commandDefaultInput::Command {
command: SecretaryCommand,
args: Vec<String>,
}
Human response to secretary questionDefaultInput::Response {
response: HumanResponse,
}
DefaultOutput
Standard output messages from secretary.
General message to userDefaultOutput::Message { content: String }
Confirm receipt and processingDefaultOutput::Acknowledgment { message: String }
Request human decisionDefaultOutput::DecisionRequired {
decision: CriticalDecision,
}
Status or completion reportDefaultOutput::Report { report: Report }
Error notificationDefaultOutput::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 new todo itempub fn add_todo(
&mut self,
content: impl Into<String>,
priority: Option<TodoPriority>,
metadata: Option<HashMap<String, String>>,
) -> GlobalResult<String>
Update todo statuspub fn update_status(
&mut self,
id: &str,
status: TodoStatus,
) -> GlobalResult<()>
Query todos by statuspub 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?;
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 to appropriate agentpub 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;
Agent capabilities/skills
Whether agent is available
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 and recording ideas
Dispatching to execution agents
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