module type export agent, sommelier, companion, virtualcustomer, agentContext, yiemAgent, run_agent, take_response, follow_up, stop_agent using Dates, UUIDs, DataStructures, JSON, NATS using GeneralUtils # ============================================================================ # Simple type aliases / definitions # ============================================================================ const Timestamp = DateTime struct Usage inputTokens::Int64 outputTokens::Int64 end # ---------------------------------------------- 100 --------------------------------------------- # # ============================================================================ # Message types # ============================================================================ abstract type agentMessage end # Base type for all agent messages struct userMessage <: agentMessage # Message from the user role::String # Always "user" content::Vector{messageContent} # Text and/or image content timestamp::Timestamp # When the message was sent end """ Create a new user message. # Arguments - `role::String`: Always "user" - `content::Vector{messageContent}`: Text and/or image content - `timestamp::Timestamp`: When the message was sent # Returns - A new `userMessage` instance # Examples ```julia julia> msg = userMessage(content=[textContent("Hello")]) userMessage("user", [textContent("Hello")], DateTime(...)) ``` """ function userMessage(; role="user", content=Vector{messageContent}(), timestamp=now()) return userMessage(role, content, timestamp) end struct assistantMessage <: agentMessage # Message from the AI assistant role::String # Always "assistant" content::Vector{messageContent} # Text and/or image content api::String # API name used (e.g., "openai") provider::String # Provider name (e.g., "anthropic") model::String # Model identifier usage::Usage # Token usage for this message stopReason::String # Why generation stopped (e.g., "end_turn") errorMessage::Union{String, Nothing} # Error if generation failed timestamp::Timestamp # When the message was received end """ Create a new assistant message. # Arguments - `role::String`: Always "assistant" - `content::Vector{messageContent}`: Text and/or image content - `api::String`: API name used (e.g., "openai") - `provider::String`: Provider name (e.g., "anthropic") - `model::String`: Model identifier - `usage::Usage`: Token usage for this message - `stopReason::String`: Why generation stopped (e.g., "end_turn") - `errorMessage::Union{String, Nothing}`: Error if generation failed - `timestamp::Timestamp`: When the message was received # Returns - A new `assistantMessage` instance # Examples ```julia julia> msg = assistantMessage(content=[textContent("Hello!")], model="gpt-4") assistantMessage("assistant", [textContent("Hello!")], "", "", "gpt-4", ..., "end_turn", nothing, DateTime(...)) ``` """ function assistantMessage(; role="assistant", content=Vector{messageContent}(), api="", provider="", model="", usage=Usage(0, 0), stopReason="end_turn", errorMessage=nothing, timestamp=now()) return assistantMessage(role, content, api, provider, model, usage, stopReason, errorMessage, timestamp) end struct toolResultMessage <: agentMessage # Result returned from a tool execution role::String # Always "tool" toolCallId::String # ID matching the tool call toolName::String # Name of the executed tool content::Vector{messageContent} # Tool output content details::Any # Additional tool-specific details usage::Union{Usage, Nothing} # Token usage if applicable addedToolNames::Union{Vector{String}, Nothing} # Tools added during execution isError::Bool # Whether the tool call resulted in an error timestamp::Timestamp # When the result was recorded end """ Create a new tool result message. # Arguments - `role::String`: Always "tool" - `toolCallId::String`: ID matching the tool call - `toolName::String`: Name of the executed tool - `content::Vector{messageContent}`: Tool output content - `details::Any`: Additional tool-specific details - `usage::Union{Usage, Nothing}`: Token usage if applicable - `addedToolNames::Union{Vector{String}, Nothing}`: Tools added during execution - `isError::Bool`: Whether the tool call resulted in an error - `timestamp::Timestamp`: When the result was recorded # Returns - A new `toolResultMessage` instance # Examples ```julia julia> msg = toolResultMessage(toolCallId="call_123", toolName="search", content=[textContent("results")]) toolResultMessage("tool", "call_123", "search", [textContent("results")], nothing, nothing, nothing, false, DateTime(...)) ``` """ function toolResultMessage(; role="tool", toolCallId="", toolName="", content=Vector{messageContent}(), details=nothing, usage=nothing, addedToolNames=nothing, isError=false, timestamp=now()) return toolResultMessage(role, toolCallId, toolName, content, details, usage, addedToolNames, isError, timestamp) end # ============================================================================ # Message content types # ============================================================================ abstract type messageContent end # Base type for message content struct textContent <: messageContent # Plain text message content text::String # The text content end """ Create plain text message content. # Arguments - `text::String`: The text content # Returns - A new `textContent` instance # Examples ```julia julia> content = textContent("Hello, world!") textContent("Hello, world!") ``` """ function textContent(; text="") return textContent(text) end struct imageContent <: messageContent # Image message content data::String # Base64-encoded image data mimeType::String # MIME type (e.g., "image/png") end """ Create image message content. # Arguments - `data::String`: Base64-encoded image data - `mimeType::String`: MIME type (e.g., "image/png") # Returns - A new `imageContent` instance # Examples ```julia julia> img = imageContent(data="base64data...", mimeType="image/png") imageContent("base64data...", "image/png") ``` """ function imageContent(; data="", mimeType="") return imageContent(data, mimeType) end # ============================================================================ # Tool types # ============================================================================ """ A tool available to the agent. # Arguments - `name::String`: Tool identifier - `label::String`: Human-readable tool name - `description::String`: What the tool does - `parameters::TParameters`: Tool parameters schema (JSON schema) - `execute::Function`: Tool execution function - `prepareArguments::Union{Function, Nothing}`: Optional argument preparation callback - `executionMode::Union{toolExecutionMode, Nothing}`: Override: run tool calls sequentially or in parallel # Returns - A new `agentTool` instance """ struct agentTool{TParameters, TDetails} # A tool available to the agent name::String # Tool identifier label::String # Human-readable tool name description::String # What the tool does parameters::TParameters # Tool parameters schema (JSON schema) execute::Function # Tool execution function prepareArguments::Union{Function, Nothing} # Optional argument preparation callback executionMode::Union{toolExecutionMode, Nothing} # Override: run tool calls sequentially or in parallel end # ============================================================================ # Agent context # ============================================================================ """ Snapshot of the agent's conversation context. # Arguments - `systemPrompt::String`: System prompt for the agent - `messages::Vector{agentMessage}`: Conversation messages - `tools::Union{Vector{agentTool}, Nothing}`: Available tools # Returns - A new `agentContext` instance """ struct agentContext # Snapshot of the agent's conversation context systemPrompt::String # System prompt for the agent messages::Vector{agentMessage} # Conversation messages tools::Union{Vector{agentTool}, Nothing} # Available tools end # ============================================================================ # Agent state # ============================================================================ mutable struct agentState # Mutable runtime state of an agent systemPrompt::String # System prompt text model::llmModel # LLM model to use tools::Vector{agentTool} # Available tools messages::Vector{agentMessage} # Conversation messages pendingToolCalls::Vector{String} # Tool call IDs waiting for results activeRun::Bool # is agent processing user message? errorMessage::Union{String, Nothing} # Last error message end """ Create a new mutable agent state. Creates a deep copy of the provided tools and messages to isolate the new state from external references. # Arguments - `systemPrompt::String`: System prompt text - `model::llmModel`: LLM model to use (defaults to an unknown model) - `tools::Vector{agentTool}`: Available tools (deep copied) - `messages::Vector{agentMessage}`: Conversation messages (deep copied) # Returns - A new `agentState` instance with an empty pending tool calls list and no error # Examples ```julia julia> state = agentState(systemPrompt="You are a helpful assistant") agentState("You are a helpful assistant", ..., agentTool[], agentMessage[], String[], nothing) ``` """ function agentState( systemPrompt::String="", model::llmModel=llmModel{String}("", "", "unknown", "unknown", "", false, String[], modelCost(0.0, 0.0, 0.0, 0.0), 0, 0), tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], ) agentState( systemPrompt, model, deepcopy(tools), deepcopy(messages), Vector{String}(), nothing, ) end struct toolCall # A tool invocation from the LLM type::String # Always "function" id::String # Unique tool call identifier name::String # Tool name arguments::Dict{String, Any} # Parsed tool arguments end """ Context for preparing the next conversation turn. # Arguments - `message::assistantMessage`: The assistant's message that just completed - `toolResults::Vector{toolResultMessage}`: Tool results from this turn - `context::agentContext`: Current conversation context - `newMessages::Vector{agentMessage}`: Messages to append to the context # Returns - A new `prepareNextTurnContext` instance """ struct prepareNextTurnContext # Context for preparing the next conversation turn message::assistantMessage # The assistant's message that just completed toolResults::Vector{toolResultMessage} # Tool results from this turn context::agentContext # Current conversation context newMessages::Vector{agentMessage} # Messages to append to the context end struct modelCost # Model pricing per 1M tokens input::Float64 # Price per 1M input tokens output::Float64 # Price per 1M output tokens cache_read::Float64 # Price per 1M cached read tokens cache_write::Float64 # Price per 1M cache write tokens end struct llmModel{Api} # LLM model configuration id::String # Unique model identifier name::String # Human-readable model name api::Api # API type (parametric type) provider::String # Provider name (e.g., "anthropic", "openai") baseUrl::String # API endpoint base URL reasoning::Bool # Whether the model supports chain-of-thought input::Vector{String} # Supported input modalities (e.g., "text", "image") cost::modelCost # Pricing information contextWindow::Int64 # Maximum context length in tokens maxTokens::Int64 # Maximum output tokens per completion end # ============================================================================ # Agent struct # ============================================================================ abstract type agent end """ docstring """ mutable struct yiemAgent <: agent # High-level agent wrapper _state::agentState # Current state (prompt, model, messages, tools, etc.) inputChannel::Channel # user sends prompt message to agent. # if agent is idle, it process user message right away. # if agent is running, it process user message after # the current tool call finished. followUpChannel::Channel # Buffers messages the user sends while the agent is busy. # Processed after all inputChannel messages are handled # and the agent is idle (not using a tool call). outputChannel::Channel # agent sends response message to user after processing # all user messages in inputChannel and all followUp messages. _task::Union{Task, Nothing} # Background task running the agent loop formatMsgForLLM::Function # Convert agent messages to LLM message format preprocessMessages ::Union{Function, Nothing} # Preprocess/transform messages before sending to LLM beforeToolCall::Union{Function, Nothing} # Callback invoked before executing a tool call afterToolCall::Union{Function, Nothing} # Callback invoked after executing a tool call prepareNextTurn::Union{Function, Nothing} # Callback to prepare the next conversation turn prepareNextTurnWithContext::Union{Function, Nothing} # Same but receives context sessionId::Union{String, Nothing} # Optional session identifier maxRetryDelayMs::Union{Int64, Nothing} # Maximum delay between retries (ms) toolExecution::toolExecutionMode # Default: run tool calls sequentially or in parallel end """ Create a new yiemAgent instance with a background loop task. Spawns a background `@spawn` task that runs the agent loop, listening on `inputChannel` and `followUpChannel` channels concurrently. # Keyword Arguments - `systemPrompt::String`: System prompt for the agent - `model`: LLM model to use - `tools::Vector{agentTool}`: Available tools (default: empty) - `messages::Vector{agentMessage}`: Initial conversation messages (default: empty) - `formatMsgForLLM::Function`: Convert agent messages to LLM message format (default: `defaultformatMsgForLLM`) - `preprocessMessages::Union{Function, Nothing}`: Preprocess/transform messages before sending to LLM (default: `nothing`) - `beforeToolCall::Union{Function, Nothing}`: Callback invoked before executing a tool call (default: `nothing`) - `afterToolCall::Union{Function, Nothing}`: Callback invoked after executing a tool call (default: `nothing`) - `prepareNextTurn::Union{Function, Nothing}`: Callback to prepare the next conversation turn (default: `nothing`) - `prepareNextTurnWithContext::Union{Function, Nothing}`: Same but receives context (default: `nothing`) - `sessionId::Union{String, Nothing}`: Optional session identifier (default: `nothing`) - `maxRetryDelayMs::Union{Int64, Nothing}`: Maximum delay between retries in milliseconds (default: `nothing`) - `toolExecution`: Default tool execution mode (sequential or parallel) (default: `nothing`) # Returns - A new `yiemAgent` instance with an active background task # Examples ```julia julia> agent = yiemAgent(systemPrompt="You are a helpful assistant", model=my_model) yiemAgent(agentState(...), Channel(...), Channel(...), Channel(...), ..., ...) ``` """ function yiemAgent( ; systemPrompt::String="", model=nothing, tools::Vector{agentTool}=agentTool[], messages::Vector{agentMessage}=agentMessage[], formatMsgForLLM::Function=defaultformatMsgForLLM, preprocessMessages::Union{Function, Nothing}=nothing, beforeToolCall::Union{Function, Nothing}=nothing, afterToolCall::Union{Function, Nothing}=nothing, prepareNextTurn::Union{Function, Nothing}=nothing, prepareNextTurnWithContext::Union{Function, Nothing}=nothing, sessionId::Union{String, Nothing}=nothing, maxRetryDelayMs::Union{Int64, Nothing}=nothing, toolExecution=nothing, ) # Create channels: input (user -> agent), followUp (async queue), output (agent -> user) inputChannel = Channel(16) followUp = Channel(32) outputChannel = Channel(16) # Create struct with a placeholder task, then spawn and replace it agent = yiemAgent( agentState(systemPrompt, model, tools, messages), inputChannel, followUp, outputChannel, nothing, # placeholder — replaced below formatMsgForLLM, preprocessMessages, beforeToolCall, afterToolCall, prepareNextTurn, prepareNextTurnWithContext, sessionId, maxRetryDelayMs, toolExecution, ) # Spawn the background loop and attach it agent._task = @spawn _agent_loop(agent) return agent end end # module type