use std::sync::Arc;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use oino_ai::{
AgentMessage, AssistantMessage, AssistantMessageEvent, Context, JsonObject, JsonValue, Message,
Model, StreamOptions, ThinkingLevel, ToolCall, ToolResultMessage, Usage, UserContent,
};
use serde::{Deserialize, Serialize};
use tokio_util::sync::CancellationToken;
use crate::tool::{AgentTool, AgentToolResult};
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{0}")]
pub struct HookError(pub String);
impl HookError {
#[must_use]
pub fn new(message: impl Into<String>) -> Self {
Self(message.into())
}
}
#[derive(Clone, Default)]
pub struct AgentContext {
pub messages: Vec<AgentMessage>,
pub tools: Vec<Arc<dyn AgentTool>>,
}
impl std::fmt::Debug for AgentContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentContext")
.field("messages", &self.messages.len())
.field(
"tools",
&self
.tools
.iter()
.map(|t| t.meta().name.clone())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl AgentContext {
#[must_use]
pub fn tool(&self, name: &str) -> Option<&Arc<dyn AgentTool>> {
self.tools.iter().find(|tool| tool.meta().name == name)
}
}
#[derive(Clone, Debug)]
pub struct StreamRequest {
pub model: Model,
pub context: Context,
pub options: StreamOptions,
pub api_key: Option<String>,
pub cancel: CancellationToken,
}
pub trait StreamFn: Send + Sync {
fn stream(&self, request: StreamRequest) -> BoxStream<'static, AssistantMessageEvent>;
}
pub type ConvertToLlm = Arc<dyn Fn(&[AgentMessage]) -> Vec<Message> + Send + Sync>;
pub trait TransformContext: Send + Sync {
fn transform<'a>(
&'a self,
messages: Vec<AgentMessage>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Vec<AgentMessage>, HookError>>;
}
pub trait GetApiKey: Send + Sync {
fn api_key<'a>(&'a self, provider: &'a str)
-> BoxFuture<'a, Result<Option<String>, HookError>>;
}
#[derive(Clone, Copy, Debug)]
pub struct BeforeToolCallContext<'a> {
pub assistant_message: &'a AssistantMessage,
pub tool_call: &'a ToolCall,
pub args: &'a JsonObject,
pub context: &'a AgentContext,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BeforeToolCallResult {
#[serde(default)]
pub block: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default)]
pub terminate: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input: Option<JsonObject>,
}
pub trait BeforeToolCall: Send + Sync {
fn before<'a>(
&'a self,
cx: BeforeToolCallContext<'a>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Option<BeforeToolCallResult>, HookError>>;
}
#[derive(Clone, Copy, Debug)]
pub struct AfterToolCallContext<'a> {
pub assistant_message: &'a AssistantMessage,
pub tool_call: &'a ToolCall,
pub args: &'a JsonObject,
pub result: &'a AgentToolResult,
pub is_error: bool,
pub context: &'a AgentContext,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AfterToolCallResult {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<UserContent>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_content: Option<JsonValue>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<Usage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub terminate: Option<bool>,
}
pub trait AfterToolCall: Send + Sync {
fn after<'a>(
&'a self,
cx: AfterToolCallContext<'a>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Option<AfterToolCallResult>, HookError>>;
}
#[derive(Clone, Copy, Debug)]
pub struct AgentTurnContext<'a> {
pub message: &'a AssistantMessage,
pub tool_results: &'a [ToolResultMessage],
pub context: &'a AgentContext,
pub new_messages: &'a [AgentMessage],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "lowercase")]
pub enum TurnDecision {
Continue,
End,
}
pub trait FinishTurn: Send + Sync {
fn finish<'a>(
&'a self,
turn: AgentTurnContext<'a>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Option<TurnDecision>, HookError>>;
}
#[derive(Clone, Debug, Default)]
pub struct AgentLoopTurnUpdate {
pub context: Option<AgentContext>,
pub messages: Option<Vec<AgentMessage>>,
pub model: Option<Model>,
pub thinking_level: Option<ThinkingLevel>,
}
pub trait PrepareNextTurn: Send + Sync {
fn prepare<'a>(
&'a self,
turn: AgentTurnContext<'a>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Option<AgentLoopTurnUpdate>, HookError>>;
}
#[derive(Clone, Copy, Debug)]
pub struct PrepareRequestContext<'a> {
pub context: &'a AgentContext,
pub model: &'a Model,
pub thinking_level: ThinkingLevel,
}
#[derive(Clone, Debug, Default)]
pub struct AgentRequestUpdate {
pub context: Option<AgentContext>,
pub model: Option<Model>,
pub thinking_level: Option<ThinkingLevel>,
}
pub trait PrepareRequest: Send + Sync {
fn prepare<'a>(
&'a self,
request: PrepareRequestContext<'a>,
cancel: &'a CancellationToken,
) -> BoxFuture<'a, Result<Option<AgentRequestUpdate>, HookError>>;
}
pub trait QueueSource: Send + Sync {
fn drain(&self) -> BoxFuture<'_, Vec<AgentMessage>>;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ToolExecution {
#[default]
Parallel,
Sequential,
}
#[derive(Clone)]
pub struct AgentLoopConfig {
pub model: Model,
pub thinking_level: ThinkingLevel,
pub stream_options: StreamOptions,
pub stream: Arc<dyn StreamFn>,
pub convert_to_llm: ConvertToLlm,
pub transform_context: Option<Arc<dyn TransformContext>>,
pub get_api_key: Option<Arc<dyn GetApiKey>>,
pub api_key: Option<String>,
pub before_tool_call: Option<Arc<dyn BeforeToolCall>>,
pub after_tool_call: Option<Arc<dyn AfterToolCall>>,
pub finish_turn: Option<Arc<dyn FinishTurn>>,
pub prepare_request: Option<Arc<dyn PrepareRequest>>,
pub prepare_next_turn: Option<Arc<dyn PrepareNextTurn>>,
pub get_steering_messages: Option<Arc<dyn QueueSource>>,
pub get_follow_up_messages: Option<Arc<dyn QueueSource>>,
pub tool_execution: ToolExecution,
}
impl AgentLoopConfig {
#[must_use]
pub fn new(model: Model, stream: Arc<dyn StreamFn>) -> Self {
Self {
model,
thinking_level: ThinkingLevel::Off,
stream_options: StreamOptions::default(),
stream,
convert_to_llm: Arc::new(default_convert_to_llm),
transform_context: None,
get_api_key: None,
api_key: None,
before_tool_call: None,
after_tool_call: None,
finish_turn: None,
prepare_request: None,
prepare_next_turn: None,
get_steering_messages: None,
get_follow_up_messages: None,
tool_execution: ToolExecution::Parallel,
}
}
}
#[must_use]
pub fn default_convert_to_llm(messages: &[AgentMessage]) -> Vec<Message> {
messages.iter().filter_map(AgentMessage::as_llm).collect()
}