use crate::agent::AgentConfig;
use crate::llm::SamplingParams;
use crate::types::{AgentId, CorrelationId, TaskId};
use acton_reactive::prelude::*;
use serde::{Deserialize, Serialize};
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct SpawnAgent {
pub config: AgentConfig,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct AgentSpawned {
pub agent_id: AgentId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct StopAgent {
pub agent_id: AgentId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct GetAgentStatus {
pub agent_id: AgentId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct AgentStatusResponse {
pub agent_id: AgentId,
pub state: String,
pub conversation_length: usize,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct RouteMessage {
pub from: AgentId,
pub to: AgentId,
pub payload: String,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct UserPrompt {
pub correlation_id: CorrelationId,
pub content: String,
}
impl UserPrompt {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self {
correlation_id: CorrelationId::new(),
content: content.into(),
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct GetStatus;
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct UpdateState {
pub new_state: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub role: MessageRole,
pub content: String,
pub tool_calls: Option<Vec<ToolCall>>,
pub tool_call_id: Option<String>,
}
impl Message {
#[must_use]
pub fn user(content: impl Into<String>) -> Self {
Self {
role: MessageRole::User,
content: content.into(),
tool_calls: None,
tool_call_id: None,
}
}
#[must_use]
pub fn assistant(content: impl Into<String>) -> Self {
Self {
role: MessageRole::Assistant,
content: content.into(),
tool_calls: None,
tool_call_id: None,
}
}
#[must_use]
pub fn assistant_with_tools(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
Self {
role: MessageRole::Assistant,
content: content.into(),
tool_calls: Some(tool_calls),
tool_call_id: None,
}
}
#[must_use]
pub fn tool(tool_call_id: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: MessageRole::Tool,
content: content.into(),
tool_calls: None,
tool_call_id: Some(tool_call_id.into()),
}
}
#[must_use]
pub fn system(content: impl Into<String>) -> Self {
Self {
role: MessageRole::System,
content: content.into(),
tool_calls: None,
tool_call_id: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum MessageRole {
System,
User,
Assistant,
Tool,
}
impl std::fmt::Display for MessageRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::System => write!(f, "system"),
Self::User => write!(f, "user"),
Self::Assistant => write!(f, "assistant"),
Self::Tool => write!(f, "tool"),
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMRequest {
pub correlation_id: CorrelationId,
pub agent_id: AgentId,
pub messages: Vec<Message>,
pub tools: Option<Vec<ToolDefinition>>,
pub sampling: Option<SamplingParams>,
}
impl LLMRequest {
#[must_use]
pub fn simple(content: impl Into<String>) -> Self {
Self {
correlation_id: CorrelationId::new(),
agent_id: AgentId::new(),
messages: vec![Message::user(content)],
tools: None,
sampling: None,
}
}
#[must_use]
pub fn with_system(system: impl Into<String>, content: impl Into<String>) -> Self {
Self {
correlation_id: CorrelationId::new(),
agent_id: AgentId::new(),
messages: vec![Message::system(system), Message::user(content)],
tools: None,
sampling: None,
}
}
#[must_use]
pub fn builder() -> LLMRequestBuilder {
LLMRequestBuilder::default()
}
}
#[derive(Default)]
pub struct LLMRequestBuilder {
correlation_id: Option<CorrelationId>,
agent_id: Option<AgentId>,
messages: Vec<Message>,
tools: Option<Vec<ToolDefinition>>,
sampling: Option<SamplingParams>,
}
impl LLMRequestBuilder {
#[must_use]
pub fn correlation_id(mut self, id: CorrelationId) -> Self {
self.correlation_id = Some(id);
self
}
#[must_use]
pub fn agent_id(mut self, id: AgentId) -> Self {
self.agent_id = Some(id);
self
}
#[must_use]
pub fn system(mut self, content: impl Into<String>) -> Self {
self.messages.push(Message::system(content));
self
}
#[must_use]
pub fn user(mut self, content: impl Into<String>) -> Self {
self.messages.push(Message::user(content));
self
}
#[must_use]
pub fn assistant(mut self, content: impl Into<String>) -> Self {
self.messages.push(Message::assistant(content));
self
}
#[must_use]
pub fn message(mut self, message: Message) -> Self {
self.messages.push(message);
self
}
#[must_use]
pub fn messages(mut self, messages: impl IntoIterator<Item = Message>) -> Self {
self.messages.extend(messages);
self
}
#[must_use]
pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools = Some(tools);
self
}
#[must_use]
pub fn tool(mut self, tool: ToolDefinition) -> Self {
self.tools.get_or_insert_with(Vec::new).push(tool);
self
}
#[must_use]
pub fn sampling(mut self, params: SamplingParams) -> Self {
self.sampling = Some(params);
self
}
#[must_use]
pub fn build(self) -> LLMRequest {
LLMRequest {
correlation_id: self.correlation_id.unwrap_or_default(),
agent_id: self.agent_id.unwrap_or_default(),
messages: self.messages,
tools: self.tools,
sampling: self.sampling,
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMResponse {
pub correlation_id: CorrelationId,
pub content: String,
pub tool_calls: Option<Vec<ToolCall>>,
pub stop_reason: StopReason,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StopReason {
EndTurn,
MaxTokens,
ToolUse,
StopSequence,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMStreamStart {
pub correlation_id: CorrelationId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMStreamToken {
pub correlation_id: CorrelationId,
pub token: String,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMStreamToolCall {
pub correlation_id: CorrelationId,
pub tool_call: ToolCall,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct LLMStreamEnd {
pub correlation_id: CorrelationId,
pub stop_reason: StopReason,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDefinition {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub id: String,
pub name: String,
pub arguments: serde_json::Value,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct ExecuteTool {
pub correlation_id: CorrelationId,
pub tool_call: ToolCall,
pub requesting_agent: AgentId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct ToolResponse {
pub correlation_id: CorrelationId,
pub tool_call_id: String,
pub result: Result<String, String>,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub enum SystemEvent {
AgentSpawned {
id: AgentId,
},
AgentStopped {
id: AgentId,
reason: String,
},
ToolRegistered {
name: String,
},
RateLimitHit {
provider: String,
retry_after_secs: u64,
},
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct AgentMessage {
pub from: AgentId,
pub to: AgentId,
pub content: String,
pub metadata: Option<serde_json::Value>,
}
impl AgentMessage {
#[must_use]
pub fn new(from: AgentId, to: AgentId, content: impl Into<String>) -> Self {
Self {
from,
to,
content: content.into(),
metadata: None,
}
}
#[must_use]
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = Some(metadata);
self
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct DelegateTask {
pub from: AgentId,
pub to: AgentId,
pub task_id: TaskId,
pub task_type: String,
pub payload: serde_json::Value,
pub deadline: Option<std::time::Duration>,
}
impl DelegateTask {
#[must_use]
pub fn new(
from: AgentId,
to: AgentId,
task_type: impl Into<String>,
payload: serde_json::Value,
) -> Self {
Self {
from,
to,
task_id: TaskId::new(),
task_type: task_type.into(),
payload,
deadline: None,
}
}
#[must_use]
pub fn with_deadline(mut self, deadline: std::time::Duration) -> Self {
self.deadline = Some(deadline);
self
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct TaskAccepted {
pub task_id: TaskId,
pub agent_id: AgentId,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct TaskCompleted {
pub task_id: TaskId,
pub result: serde_json::Value,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct TaskFailed {
pub task_id: TaskId,
pub error: String,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct AnnounceCapabilities {
pub agent_id: AgentId,
pub capabilities: Vec<String>,
}
impl AnnounceCapabilities {
#[must_use]
pub fn new(agent_id: AgentId, capabilities: Vec<String>) -> Self {
Self {
agent_id,
capabilities,
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct FindCapableAgent {
pub capability: String,
pub correlation_id: CorrelationId,
}
impl FindCapableAgent {
#[must_use]
pub fn new(capability: impl Into<String>) -> Self {
Self {
capability: capability.into(),
correlation_id: CorrelationId::new(),
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct CapableAgentFound {
pub correlation_id: CorrelationId,
pub agent_id: Option<AgentId>,
pub capability: String,
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct IncomingAgentMessage {
pub from: AgentId,
pub content: String,
pub metadata: Option<serde_json::Value>,
}
impl From<AgentMessage> for IncomingAgentMessage {
fn from(msg: AgentMessage) -> Self {
Self {
from: msg.from,
content: msg.content,
metadata: msg.metadata,
}
}
}
#[acton_message]
#[derive(Serialize, Deserialize)]
pub struct IncomingTask {
pub from: AgentId,
pub task_id: TaskId,
pub task_type: String,
pub payload: serde_json::Value,
pub deadline: Option<std::time::Duration>,
}
impl IncomingTask {
#[must_use]
pub fn from_delegate(msg: &DelegateTask) -> Self {
Self {
from: msg.from.clone(),
task_id: msg.task_id.clone(),
task_type: msg.task_type.clone(),
payload: msg.payload.clone(),
deadline: msg.deadline,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_prompt_creates_correlation_id() {
let prompt1 = UserPrompt::new("Hello");
let prompt2 = UserPrompt::new("World");
assert_ne!(prompt1.correlation_id, prompt2.correlation_id);
}
#[test]
fn message_user_creation() {
let msg = Message::user("Hello, agent!");
assert_eq!(msg.role, MessageRole::User);
assert_eq!(msg.content, "Hello, agent!");
assert!(msg.tool_calls.is_none());
assert!(msg.tool_call_id.is_none());
}
#[test]
fn message_assistant_creation() {
let msg = Message::assistant("I can help with that.");
assert_eq!(msg.role, MessageRole::Assistant);
assert_eq!(msg.content, "I can help with that.");
}
#[test]
fn message_assistant_with_tools() {
let tool_calls = vec![ToolCall {
id: "tc_123".to_string(),
name: "search".to_string(),
arguments: serde_json::json!({"query": "Rust actors"}),
}];
let msg = Message::assistant_with_tools("Let me search for that.", tool_calls);
assert_eq!(msg.role, MessageRole::Assistant);
assert!(msg.tool_calls.is_some());
assert_eq!(msg.tool_calls.as_ref().unwrap().len(), 1);
}
#[test]
fn message_tool_response() {
let msg = Message::tool("tc_123", "Search results: ...");
assert_eq!(msg.role, MessageRole::Tool);
assert_eq!(msg.tool_call_id, Some("tc_123".to_string()));
}
#[test]
fn message_role_display() {
assert_eq!(MessageRole::System.to_string(), "system");
assert_eq!(MessageRole::User.to_string(), "user");
assert_eq!(MessageRole::Assistant.to_string(), "assistant");
assert_eq!(MessageRole::Tool.to_string(), "tool");
}
#[test]
fn tool_definition_serialization() {
let tool = ToolDefinition {
name: "calculator".to_string(),
description: "Performs basic arithmetic".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"expression": {"type": "string"}
},
"required": ["expression"]
}),
};
let json = serde_json::to_string(&tool).unwrap();
let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(tool, deserialized);
}
#[test]
fn system_event_agent_spawned() {
let agent_id = AgentId::new();
let event = SystemEvent::AgentSpawned {
id: agent_id.clone(),
};
if let SystemEvent::AgentSpawned { id } = event {
assert_eq!(id, agent_id);
} else {
panic!("Expected AgentSpawned event");
}
}
#[test]
fn stop_reason_serialization() {
let reasons = vec![
StopReason::EndTurn,
StopReason::MaxTokens,
StopReason::ToolUse,
StopReason::StopSequence,
];
for reason in reasons {
let json = serde_json::to_string(&reason).unwrap();
let deserialized: StopReason = serde_json::from_str(&json).unwrap();
assert_eq!(reason, deserialized);
}
}
#[test]
fn llm_request_simple_creates_user_message() {
let request = LLMRequest::simple("Hello");
assert_eq!(request.messages.len(), 1);
assert_eq!(request.messages[0].role, MessageRole::User);
assert_eq!(request.messages[0].content, "Hello");
assert!(request.tools.is_none());
}
#[test]
fn llm_request_simple_generates_ids() {
let request1 = LLMRequest::simple("Hello");
let request2 = LLMRequest::simple("World");
assert_ne!(request1.correlation_id, request2.correlation_id);
assert_ne!(request1.agent_id, request2.agent_id);
}
#[test]
fn llm_request_with_system_creates_two_messages() {
let request = LLMRequest::with_system("Be helpful", "Hello");
assert_eq!(request.messages.len(), 2);
assert_eq!(request.messages[0].role, MessageRole::System);
assert_eq!(request.messages[0].content, "Be helpful");
assert_eq!(request.messages[1].role, MessageRole::User);
assert_eq!(request.messages[1].content, "Hello");
}
#[test]
fn llm_request_builder_basic() {
let request = LLMRequest::builder().user("Hello").build();
assert_eq!(request.messages.len(), 1);
assert_eq!(request.messages[0].content, "Hello");
}
#[test]
fn llm_request_builder_with_system_and_user() {
let request = LLMRequest::builder()
.system("Be concise")
.user("What is 2+2?")
.build();
assert_eq!(request.messages.len(), 2);
assert_eq!(request.messages[0].role, MessageRole::System);
assert_eq!(request.messages[1].role, MessageRole::User);
}
#[test]
fn llm_request_builder_with_explicit_ids() {
let corr_id = CorrelationId::new();
let agent_id = AgentId::new();
let request = LLMRequest::builder()
.correlation_id(corr_id.clone())
.agent_id(agent_id.clone())
.user("Hello")
.build();
assert_eq!(request.correlation_id, corr_id);
assert_eq!(request.agent_id, agent_id);
}
#[test]
fn llm_request_builder_with_tools() {
let tool = ToolDefinition {
name: "calculator".to_string(),
description: "Math".to_string(),
input_schema: serde_json::json!({}),
};
let request = LLMRequest::builder()
.user("Calculate 2+2")
.tool(tool.clone())
.build();
assert!(request.tools.is_some());
assert_eq!(request.tools.as_ref().unwrap().len(), 1);
assert_eq!(request.tools.as_ref().unwrap()[0].name, "calculator");
}
#[test]
fn llm_request_builder_with_multiple_tools() {
let tools = vec![
ToolDefinition {
name: "calc".to_string(),
description: "Math".to_string(),
input_schema: serde_json::json!({}),
},
ToolDefinition {
name: "search".to_string(),
description: "Search".to_string(),
input_schema: serde_json::json!({}),
},
];
let request = LLMRequest::builder().user("Hello").tools(tools).build();
assert!(request.tools.is_some());
assert_eq!(request.tools.as_ref().unwrap().len(), 2);
}
#[test]
fn llm_request_builder_with_assistant() {
let request = LLMRequest::builder()
.user("Hello")
.assistant("Hi there!")
.user("How are you?")
.build();
assert_eq!(request.messages.len(), 3);
assert_eq!(request.messages[1].role, MessageRole::Assistant);
}
#[test]
fn llm_request_builder_with_custom_message() {
let custom_msg = Message::tool("tc_123", "Result: 4");
let request = LLMRequest::builder()
.user("Calculate 2+2")
.message(custom_msg)
.build();
assert_eq!(request.messages.len(), 2);
assert_eq!(request.messages[1].role, MessageRole::Tool);
}
#[test]
fn llm_request_builder_generates_ids_when_not_set() {
let request1 = LLMRequest::builder().user("Hello").build();
let request2 = LLMRequest::builder().user("World").build();
assert_ne!(request1.correlation_id, request2.correlation_id);
assert_ne!(request1.agent_id, request2.agent_id);
}
}