LemonAgent (lemon_agent v0.1.0)
View SourceHigh-level agent execution library built on top of the LemonAi library.
LemonAgent provides a complete framework for building AI agents that can:
- Execute multi-turn conversations with tool use
- Stream responses with fine-grained event notifications
- Handle complex agentic loops (prompt -> response -> tool calls -> results -> repeat)
- Manage agent state and lifecycle as a GenServer
Relationship to the LemonAi Library
The LemonAi library provides low-level LLM API abstractions - streaming, message types,
provider implementations, etc. LemonAgent builds on top of this to provide:
- Agent Loop - The core loop that handles tool execution and multi-turn conversations
- Agent GenServer - A supervised process that manages agent state and handles concurrency
- Event System - Rich events for UI integration and progress tracking
- Extended Types - Agent-specific types like
AgentToolwith execute functions
┌─────────────────────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LemonAgent │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Agent │ │ Loop │ │ EventStream / Types │ │
│ │ (GenServer)│ │ (core logic)│ │ (events & structures) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LemonAi Library │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ stream/3 │ │ Types │ │ Providers │ │
│ │ complete/3 │ │ Context │ │ (Anthropic, OpenAI,...) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Quick Start
Using the Agent GenServer (recommended)
The Agent GenServer provides a supervised, stateful interface with event subscriptions:
# Start an agent
{:ok, agent} = LemonAgent.new_agent(
model: my_model,
system_prompt: "You are a helpful assistant.",
tools: [read_file_tool, write_file_tool]
)
# Subscribe to events
LemonAgent.subscribe(agent, self())
# Send a prompt (non-blocking)
:ok = LemonAgent.prompt(agent, "Hello, can you help me?")
# Receive events as they arrive
receive do
{:agent_event, {:message_start, msg}} ->
IO.puts("Assistant started responding...")
{:agent_event, {:message_update, msg, delta}} ->
IO.write(delta)
{:agent_event, {:tool_execution_start, id, name, args}} ->
IO.puts("Executing tool: #{name}")
{:agent_event, {:agent_end, messages}} ->
IO.puts("Agent finished with #{length(messages)} messages")
end
# Or wait for the agent to finish
:ok = LemonAgent.wait_for_idle(agent)
state = LemonAgent.get_state(agent)Using the Loop directly (advanced)
For more control, you can use the Loop module directly:
alias LemonAgent.{Loop, Types}
# Create initial state
state = %Types.AgentState{
system_prompt: "You are helpful",
model: my_model,
tools: my_tools,
messages: []
}
# Create config with required callbacks
config = %Types.AgentLoopConfig{
model: my_model,
convert_to_llm: &my_convert_fn/1
}
# Run the loop (returns a stream of events)
state
|> Loop.agent_loop(config, user_message)
|> Enum.each(fn event ->
IO.inspect(event)
end)Events
LemonAgent emits detailed events throughout agent execution:
Agent Lifecycle
{:agent_start}- Agent run has begun{:tool_schema_snapshot, snapshot}- Tool schema was frozen for the run{:agent_end, messages}- Agent run completed with final message list
Turn Lifecycle
{:turn_start}- New turn (LLM call) has started{:turn_end, message, tool_results}- Turn completed
Message Lifecycle
{:message_start, message}- Message processing started{:message_update, message, delta}- Streaming update{:message_end, message}- Message processing complete
Tool Execution
{:tool_execution_start, id, name, args}- Tool started{:tool_execution_update, id, name, args, partial}- Partial result{:tool_execution_end, id, name, result, is_error}- Tool completed
Configuration
The AgentLoopConfig struct controls agent behavior:
%LemonAgent.Types.AgentLoopConfig{
# Required: the model to use
model: my_model,
# Required: convert agent messages to LLM format
convert_to_llm: fn messages -> {:ok, llm_messages} end,
# Optional: transform context before each LLM call
# (useful for context window management)
transform_context: fn messages, signal -> {:ok, transformed} end,
# Optional: get API key dynamically (for OAuth tokens)
get_api_key: fn provider -> api_key end,
# Optional: inject steering messages mid-run
get_steering_messages: fn -> [] end,
# Optional: add follow-up prompts to keep agent running
get_follow_up_messages: fn -> [] end,
# Optional: streaming options
stream_options: %LemonAi.Types.StreamOptions{}
}Modules
LemonAgent.Agent- GenServer for stateful agent managementLemonAgent.Loop- Core agentic loop implementationLemonAgent.EventStream- Async event producer/consumerLemonAgent.Types- Type definitions and structsLemonAgent.Proxy- Stream proxy for event transformation
Summary
Types
Reference to an Agent GenServer process
Configuration for the agent loop
Context for agent conversations
Events emitted during agent execution
Agent state containing messages, tools, and configuration
Thinking/reasoning level
Tool definition with execute function
Result from tool execution
Functions
Abort the current agent run.
Run the agent loop starting with prompts.
Continue the agent loop without adding a new user message.
Continue a paused agent run.
Get the current agent state.
Extract text from a tool result or message content.
Create an image content block.
Start a new Agent GenServer.
Create a new agent context.
Create a new agent tool.
Create a new tool result.
Send a prompt to the agent.
Reset the agent state, clearing all messages.
Start and link an Agent GenServer.
Subscribe to agent events.
Create a text content block.
Block until the agent is idle (not processing).
Types
@type agent() :: GenServer.server()
Reference to an Agent GenServer process
@type config() :: LemonAgent.Types.AgentLoopConfig.t()
Configuration for the agent loop
@type context() :: LemonAgent.Types.AgentContext.t()
Context for agent conversations
@type event() :: LemonAgent.Types.agent_event()
Events emitted during agent execution
@type state() :: LemonAgent.Types.AgentState.t()
Agent state containing messages, tools, and configuration
@type thinking_level() :: LemonAgent.Types.thinking_level()
Thinking/reasoning level
@type tool() :: LemonAgent.Types.AgentTool.t()
Tool definition with execute function
@type tool_result() :: LemonAgent.Types.AgentToolResult.t()
Result from tool execution
Functions
@spec abort(agent()) :: :ok
Abort the current agent run.
This signals cancellation to any running tool executions and stops the agent loop.
@spec agent_loop( [LemonAgent.Types.agent_message()], context(), config(), function() | nil ) :: Enumerable.t()
Run the agent loop starting with prompts.
This is the core function that implements the agentic loop:
- Send message to LLM
- Process response
- Execute any tool calls
- If tool calls were made, loop back to step 1
- When no tool calls, emit agent_end and return
Returns a Stream that emits events as they occur.
Parameters
prompts- List of messages to start withcontext- AgentContext with system prompt and toolsconfig- AgentLoopConfig with model and callbacksstream_fn- Optional custom stream function (default: LemonAi.stream/3)
Examples
context
|> LemonAgent.agent_loop([user_msg], config)
|> Enum.each(&handle_event/1)
@spec agent_loop_continue(context(), config(), function() | nil) :: Enumerable.t()
Continue the agent loop without adding a new user message.
Used for continuing after tool results have been added to context.
Parameters
context- AgentContext with messages including tool resultsconfig- AgentLoopConfig with model and callbacksstream_fn- Optional custom stream function
Continue a paused agent run.
Use this after handling required user input or approval.
Get the current agent state.
Returns the full AgentState struct including messages, tools,
streaming status, and any errors.
@spec get_text( tool_result() | [LemonAi.Types.TextContent.t() | LemonAi.Types.ImageContent.t()] ) :: String.t()
Extract text from a tool result or message content.
Examples
text = LemonAgent.get_text(tool_result)
@spec image_content(String.t(), String.t()) :: LemonAi.Types.ImageContent.t()
Create an image content block.
Examples
content = LemonAgent.image_content(base64_data, "image/png")
@spec new_agent(keyword()) :: GenServer.on_start()
Start a new Agent GenServer.
This is an alias for LemonAgent.Agent.start_link/1.
Options
:model- (required) The AI model to use (LemonAi.Types.Model.t()):system_prompt- System prompt for the agent (default: ""):tools- List ofAgentToolstructs (default: []):thinking_level- Extended reasoning level (default::off):convert_to_llm- Function to convert agent messages to LLM format:transform_context- Optional context transformation function:get_api_key- Optional function to resolve API keys dynamically:stream_options- Options for streaming requests:max_tool_turns- Max assistant tool-use turns before terminal fallback
Examples
{:ok, agent} = LemonAgent.new_agent(
model: claude_model,
system_prompt: "You are a helpful coding assistant.",
tools: [read_tool, write_tool, execute_tool]
)
Create a new agent context.
Options
:system_prompt- System prompt for the conversation:messages- Initial messages (default: []):tools- Available tools (default: [])
Examples
context = LemonAgent.new_context(
system_prompt: "You are helpful",
tools: [my_tool]
)
Create a new agent tool.
Fields
:name- (required) Tool name for LLM to call:description- (required) What the tool does:parameters- JSON Schema for parameters (default: %{}):label- Human-readable label for UI (default: same as name):execute- (required) Function to execute the tool
The execute function receives:
tool_call_id- Unique ID for this invocationparams- Parsed parameters from the tool callsignal- Abort signal reference (or nil)on_update- Callback for streaming partial results
Examples
read_tool = LemonAgent.new_tool(
name: "read_file",
description: "Read the contents of a file",
parameters: %{
"type" => "object",
"properties" => %{
"path" => %{"type" => "string", "description" => "File path"}
},
"required" => ["path"]
},
label: "Read File",
execute: fn _id, %{"path" => path}, _signal, _on_update ->
case File.read(path) do
{:ok, content} ->
%LemonAgent.Types.AgentToolResult{
content: [%LemonAi.Types.TextContent{text: content}]
}
{:error, reason} ->
{:error, reason}
end
end
)
@spec new_tool_result(keyword()) :: tool_result()
Create a new tool result.
Options
:content- List of content blocks (default: []):details- Optional details for logging/UI:trust- Tool result trust level,:trustedor:untrusted(default::trusted)
Examples
result = LemonAgent.new_tool_result(
content: [%LemonAi.Types.TextContent{text: "File contents here"}],
details: %{bytes_read: 1024}
)
@spec prompt( agent(), String.t() | LemonAgent.Types.agent_message() | [LemonAgent.Types.agent_message()] ) :: :ok | {:error, term()}
Send a prompt to the agent.
This starts a new agent run. The agent will process the prompt, potentially make tool calls, and continue until it completes or is aborted.
Examples
:ok = LemonAgent.prompt(agent, "What files are in the current directory?")
@spec reset(agent()) :: :ok
Reset the agent state, clearing all messages.
@spec start_link(keyword()) :: GenServer.on_start()
Start and link an Agent GenServer.
See new_agent/1 for options.
Subscribe to agent events.
The subscriber will receive messages in the format:
{:agent_event, event}where event is one of the agent event types.
Examples
LemonAgent.subscribe(agent, self())
receive do
{:agent_event, {:agent_end, messages}} ->
IO.puts("Done!")
end
@spec text_content(String.t()) :: LemonAi.Types.TextContent.t()
Create a text content block.
Convenience wrapper for LemonAi.Types.TextContent.
Examples
content = LemonAgent.text_content("Hello, world!")
Block until the agent is idle (not processing).
Options
:timeout- How long to wait (default::infinity)
Examples
:ok = LemonAgent.wait_for_idle(agent)
state = LemonAgent.get_state(agent)