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 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 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 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 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 function imageContent(; data="", mimeType="") return imageContent(data, mimeType) end # ============================================================================ # Tool types # ============================================================================ 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 # ============================================================================ 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 errorMessage::Union{String, Nothing} # Last error message end 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 # ============================================================================ # Tool call types # ============================================================================ 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 # ============================================================================ # Next turn context # ============================================================================ struct nextTurnContext # 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 # ============================================================================ # llmModel types # ============================================================================ 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.) input_ch::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. followUpQueue::Channel # Messages queued via follow_up() during agent is # running. After the agent loop process all input_ch # and the agent isn't using tool call, it processes # followUp messages output_ch::Channel # agent sends response message to user after processing # all user messages in input_ch 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 """ docstring """ 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) input_ch = Channel(16) followUp = Channel(32) output_ch = Channel(16) # Create struct with a placeholder task, then spawn and replace it agent = yiemAgent( agentState(systemPrompt, model, tools, messages), input_ch, followUp, output_ch, 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 # ============================================================================ # Agent loop — runs in background, processes messages from input_ch / followUp # ============================================================================ """ Private agent loop. Runs in a background @task. Waits on input_ch and followUpQueue concurrently via select(). """ function _agent_loop(agent::yiemAgent) #WORKING try while true # Wait on either channel — the one with a message fires first msg = select(agent.input_ch, agent.followUpQueue).val # Check for shutdown signal if msg === :shutdown break end # Dispatch message through the processing pipeline result = _process_message(agent, msg) # Send response to user put!(agent.output_ch, result) end catch e # On any error, send error response and exit the loop @error "Agent loop failed" error=e end end """ Process a single message through the agent pipeline. This is where you add your LLM call, tool execution, etc. """ function _process_message(agent::yiemAgent, msg) # PENDING Replace with actual processing logic # # 1. Add msg to agent._state.messages # 2. Call agent.formatMsgForLLM(agent._state) to format for LLM # 3. If preprocessMessages is set, call agent.preprocessMessages(...) # 4. Call the LLM (blocking — the task waits here) # 5. If agent has tools, handle tool calls in a loop # 6. Build assistantMessage and return it # Placeholder: echo back the message as a simple response @warn "TODO: implement _process_message" return assistantMessage( role="assistant", content=[textContent("Received: $(msg)")], api="", model="", usage=nothing, stopReason="end_turn", errorMessage=nothing, timestamp=now(), ) end # ============================================================================ # Public API — interaction helpers # ============================================================================ """ Send a message to the agent's input channel. Blocks if the input channel buffer is full (capacity 16 by default). """ function run_agent(agent::yiemAgent, msg) put!(agent.input_ch, msg) return agent end """ Take a response from the agent's output channel. Blocks until the agent sends a response. """ function take_response(agent::yiemAgent) return take!(agent.output_ch) end """ Send a follow-up message while the agent is still processing. Follow-up messages are processed after all input_ch messages and before any tool call results are sent. """ function follow_up(agent::yiemAgent, msg) put!(agent.followUpQueue, msg) return agent end """ Gracefully stop the agent. Sends a :shutdown signal, waits for the task to finish, then closes all channels. """ function stop_agent(agent::yiemAgent) put!(agent.input_ch, :shutdown) try fetch(agent._task) catch e if e isa TaskFailedException rethrow(e) end end close(agent.input_ch) close(agent.output_ch) close(agent.followUpQueue) return nothing end end # module type