use std::sync::Arc;
use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use brainwires_core::{Message, Role, ToolResult, ToolUse, estimate_tokens_from_size};
use crate::pool::AgentPool;
use crate::task_agent::TaskAgentConfig;
#[derive(Debug, Clone)]
pub struct IterationContext<'a> {
pub agent_id: &'a str,
pub iteration: u32,
pub max_iterations: u32,
pub total_tokens_used: u64,
pub total_cost_usd: f64,
pub elapsed: Duration,
pub conversation_len: usize,
}
#[derive(Debug, Clone)]
pub enum IterationDecision {
Continue,
Skip,
Abort(String),
}
#[derive(Debug, Clone)]
pub enum ToolDecision {
Execute,
Override(ToolResult),
Delegate(Box<DelegationRequest>),
}
#[derive(Debug, Clone)]
pub struct DelegationRequest {
pub task_description: String,
pub config: Option<TaskAgentConfig>,
pub seed_messages: Vec<Message>,
pub blocking: bool,
}
impl Default for DelegationRequest {
fn default() -> Self {
Self {
task_description: String::new(),
config: None,
seed_messages: Vec::new(),
blocking: true,
}
}
}
#[derive(Debug, Clone)]
pub struct DelegationResult {
pub agent_id: String,
pub success: bool,
pub output: String,
pub iterations_used: u32,
pub tokens_used: u64,
}
pub struct ConversationView<'a> {
messages: &'a mut Vec<Message>,
}
impl<'a> ConversationView<'a> {
pub fn new(messages: &'a mut Vec<Message>) -> Self {
Self { messages }
}
pub fn len(&self) -> usize {
self.messages.len()
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn messages(&self) -> &[Message] {
self.messages
}
pub fn last_n(&self, n: usize) -> &[Message] {
let start = self.messages.len().saturating_sub(n);
&self.messages[start..]
}
pub fn push(&mut self, msg: Message) {
self.messages.push(msg);
}
pub fn insert(&mut self, index: usize, msg: Message) {
self.messages.insert(index, msg);
}
pub fn drain(&mut self, range: std::ops::Range<usize>) -> Vec<Message> {
self.messages.drain(range).collect()
}
pub fn summarize_range(&mut self, range: std::ops::Range<usize>, summary: Message) {
let start = range.start;
self.messages.drain(range);
self.messages.insert(start, summary);
}
pub fn estimated_tokens(&self) -> u64 {
self.messages
.iter()
.map(|m| {
let bytes = match &m.content {
brainwires_core::MessageContent::Text(t) => t.len() as u64,
brainwires_core::MessageContent::Blocks(blocks) => {
blocks.iter().map(|b| format!("{:?}", b).len() as u64).sum()
}
};
estimate_tokens_from_size(bytes) as u64
})
.sum()
}
pub fn last_assistant_text(&self) -> Option<&str> {
self.messages
.iter()
.rev()
.find(|m| m.role == Role::Assistant)
.and_then(|m| m.text())
}
}
#[async_trait]
pub trait AgentLifecycleHooks: Send + Sync {
async fn on_before_iteration(
&self,
_ctx: &IterationContext<'_>,
_conversation: &mut ConversationView<'_>,
) -> IterationDecision {
IterationDecision::Continue
}
async fn on_after_iteration(
&self,
_ctx: &IterationContext<'_>,
_conversation: &mut ConversationView<'_>,
) {
}
async fn on_before_provider_call(
&self,
_ctx: &IterationContext<'_>,
_conversation: &mut ConversationView<'_>,
) {
}
async fn on_after_provider_call(
&self,
_ctx: &IterationContext<'_>,
_response: &brainwires_core::ChatResponse,
) {
}
async fn on_before_tool_execution(
&self,
_ctx: &IterationContext<'_>,
_tool_use: &ToolUse,
) -> ToolDecision {
ToolDecision::Execute
}
async fn on_after_tool_execution(
&self,
_ctx: &IterationContext<'_>,
_tool_use: &ToolUse,
_result: &ToolResult,
_conversation: &mut ConversationView<'_>,
) {
}
async fn on_before_completion(
&self,
_ctx: &IterationContext<'_>,
_completion_text: &str,
) -> bool {
true
}
async fn on_after_completion(
&self,
_ctx: &IterationContext<'_>,
_result: &crate::task_agent::TaskAgentResult,
) {
}
async fn on_context_pressure(
&self,
_ctx: &IterationContext<'_>,
_conversation: &mut ConversationView<'_>,
_estimated_tokens: u64,
_budget_tokens: u64,
) {
}
async fn execute_delegation(&self, _request: &DelegationRequest) -> Result<DelegationResult> {
Err(anyhow::anyhow!(
"Delegation unsupported by this hook provider. \
Override execute_delegation() or use DefaultDelegationHandler."
))
}
}
pub struct DefaultDelegationHandler {
pool: Arc<AgentPool>,
}
impl DefaultDelegationHandler {
pub fn new(pool: Arc<AgentPool>) -> Self {
Self { pool }
}
pub async fn delegate(&self, request: &DelegationRequest) -> Result<DelegationResult> {
let task = brainwires_core::Task::new(
uuid::Uuid::new_v4().to_string(),
request.task_description.clone(),
);
let agent_id = self.pool.spawn_agent(task, request.config.clone()).await?;
if request.blocking {
let result = self.pool.await_completion(&agent_id).await?;
Ok(DelegationResult {
agent_id: result.agent_id,
success: result.success,
output: result.summary,
iterations_used: result.iterations,
tokens_used: result.total_tokens_used,
})
} else {
Ok(DelegationResult {
agent_id,
success: true,
output: "Delegation started (non-blocking)".to_string(),
iterations_used: 0,
tokens_used: 0,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use brainwires_core::{Message, MessageContent, Role};
#[test]
fn test_conversation_view_len_and_empty() {
let mut msgs = vec![];
let view = ConversationView::new(&mut msgs);
assert!(view.is_empty());
assert_eq!(view.len(), 0);
}
#[test]
fn test_conversation_view_push_and_messages() {
let mut msgs = vec![];
let mut view = ConversationView::new(&mut msgs);
view.push(Message::user("hello"));
view.push(Message::user("world"));
assert_eq!(view.len(), 2);
assert_eq!(view.messages()[0].text(), Some("hello"));
}
#[test]
fn test_conversation_view_last_n() {
let mut msgs = vec![Message::user("a"), Message::user("b"), Message::user("c")];
let view = ConversationView::new(&mut msgs);
assert_eq!(view.last_n(2).len(), 2);
assert_eq!(view.last_n(2)[0].text(), Some("b"));
assert_eq!(view.last_n(100).len(), 3); }
#[test]
fn test_conversation_view_drain() {
let mut msgs = vec![
Message::user("keep"),
Message::user("remove1"),
Message::user("remove2"),
Message::user("keep2"),
];
let mut view = ConversationView::new(&mut msgs);
let removed = view.drain(1..3);
assert_eq!(removed.len(), 2);
assert_eq!(view.len(), 2);
assert_eq!(view.messages()[0].text(), Some("keep"));
assert_eq!(view.messages()[1].text(), Some("keep2"));
}
#[test]
fn test_conversation_view_summarize_range() {
let mut msgs = vec![
Message::user("first"),
Message::user("old1"),
Message::user("old2"),
Message::user("old3"),
Message::user("last"),
];
let mut view = ConversationView::new(&mut msgs);
view.summarize_range(1..4, Message::user("[summary of 3 messages]"));
assert_eq!(view.len(), 3);
assert_eq!(view.messages()[0].text(), Some("first"));
assert_eq!(view.messages()[1].text(), Some("[summary of 3 messages]"));
assert_eq!(view.messages()[2].text(), Some("last"));
}
#[test]
fn test_conversation_view_estimated_tokens() {
let mut msgs = vec![Message::user("hello world, this is a test message")];
let view = ConversationView::new(&mut msgs);
let tokens = view.estimated_tokens();
assert!(tokens > 0);
}
#[test]
fn test_conversation_view_last_assistant_text() {
let mut msgs = vec![
Message::user("question"),
Message {
role: Role::Assistant,
content: MessageContent::Text("answer".to_string()),
name: None,
metadata: None,
},
Message::user("follow-up"),
];
let view = ConversationView::new(&mut msgs);
assert_eq!(view.last_assistant_text(), Some("answer"));
}
#[test]
fn test_conversation_view_insert() {
let mut msgs = vec![Message::user("first"), Message::user("last")];
let mut view = ConversationView::new(&mut msgs);
view.insert(1, Message::user("middle"));
assert_eq!(view.len(), 3);
assert_eq!(view.messages()[1].text(), Some("middle"));
}
#[test]
fn test_iteration_decision_variants() {
let _continue = IterationDecision::Continue;
let _skip = IterationDecision::Skip;
let _abort = IterationDecision::Abort("reason".to_string());
}
#[test]
fn test_tool_decision_variants() {
let _execute = ToolDecision::Execute;
let _override =
ToolDecision::Override(ToolResult::success("id".to_string(), "ok".to_string()));
let _delegate = ToolDecision::Delegate(Box::new(DelegationRequest::default()));
}
#[test]
fn test_delegation_request_default() {
let req = DelegationRequest::default();
assert!(req.task_description.is_empty());
assert!(req.config.is_none());
assert!(req.seed_messages.is_empty());
assert!(req.blocking);
}
}