LemonAgent. EventStream
(lemon_agent v0.1.0)
View Source
Async event stream for streaming Agent responses with OTP-compliant lifecycle management.
This module provides a producer/consumer pattern for streaming events from agent execution with the following BEAM/OTP guarantees:
Features
- Owner Monitoring: Streams are linked to an owner process and automatically cancel when the owner dies.
- Task Linking: Streaming tasks can be attached and are properly shutdown when the stream is canceled.
- Bounded Queues: Configurable queue limits prevent unbounded memory growth.
Backpressure:
push/2returns:ok | {:error, :overflow}for flow control.- Cancellation: Explicit
cancel/2API for clean stream termination. - Timeouts: Configurable stream timeout with automatic cancellation.
Usage
# Start a stream with options
{:ok, stream} = EventStream.start_link(
owner: self(),
max_queue: 1000,
timeout: 300_000
)
# Producer pushes events (with backpressure)
case EventStream.push(stream, {:tool_call, %{name: "read_file"}}) do
:ok -> :continue
{:error, :overflow} -> :stop_producing
end
# Or use async push for fire-and-forget
EventStream.push_async(stream, {:agent_start})
# Consumer reads events
stream
|> EventStream.events()
|> Enum.each(fn event -> IO.inspect(event) end)
# Get the final result
{:ok, messages} = EventStream.result(stream)
# Or cancel explicitly
EventStream.cancel(stream, :user_requested)Terminal Events
The stream terminates when one of these events is received:
{:agent_end, messages}- Successful completion with final message list{:error, reason, partial_state}- Error with reason and any partial state{:canceled, reason}- Stream was canceled
Summary
Functions
Attach a task to this event stream.
Cancel the stream with a reason.
Returns a specification to start this module under a supervisor.
Complete the stream with a final list of messages.
Signal an error on the stream.
Get a lazy enumerable of events from the stream.
Push an event to the stream (synchronous with backpressure).
Push an event to the stream (asynchronous, fire-and-forget).
Get the final result of the stream, blocking until complete.
Start a new event stream without linking to the caller.
Start a new event stream.
Get current queue statistics.
Types
@type drop_strategy() :: :drop_oldest | :drop_newest | :error
@type event() :: {:agent_start, map()} | {:agent_end, list()} | {:tool_call, map()} | {:tool_result, map()} | {:thinking, String.t()} | {:text_delta, String.t()} | {:error, term(), term()} | {:canceled, term()} | term()
Agent events that can be pushed to the stream.
Terminal events:
{:agent_end, messages}- Successful completion{:error, reason, partial_state}- Error state{:canceled, reason}- Stream was canceled
Non-terminal events can be any term representing agent activity.
@type option() :: {:owner, pid()} | {:runner, pid()} | {:max_queue, pos_integer()} | {:drop_strategy, drop_strategy()} | {:timeout, timeout()}
@type t() :: pid()
Functions
Attach a task to this event stream.
When the stream is canceled or the owner dies, the attached task will be shutdown. Only one task can be attached at a time.
Cancel the stream with a reason.
This will:
- Mark the stream as canceled
- Shutdown any attached task
- Wake up all waiters with a terminal event
- Stop the GenServer
Examples
:ok = EventStream.cancel(stream, :user_requested)
Returns a specification to start this module under a supervisor.
See Supervisor.
Complete the stream with a final list of messages.
This pushes an {:agent_end, messages} terminal event and marks
the stream as done. After completion, result/1 will return
{:ok, messages}.
Examples
:ok = EventStream.complete(stream, [user_msg, assistant_msg])
Signal an error on the stream.
This pushes an {:error, reason, partial_state} terminal event and marks
the stream as done. After an error, result/1 will return
{:error, reason, partial_state}.
Examples
:ok = EventStream.error(stream, :timeout, %{messages: partial_messages})
@spec events(t()) :: Enumerable.t()
Get a lazy enumerable of events from the stream.
This returns a Stream that will block when no events are available
and complete when a terminal event is received. Terminal events
({:agent_end, _}, {:error, _, _}, or {:canceled, _}) are included
in the stream before it halts.
Examples
stream
|> EventStream.events()
|> Enum.each(fn
{:agent_end, messages} -> IO.puts("Done with #{length(messages)} messages")
{:tool_call, call} -> IO.puts("Calling tool: #{call.name}")
event -> IO.inspect(event)
end)
Push an event to the stream (synchronous with backpressure).
Returns :ok on success or {:error, :overflow} if the queue is full
(when using :error drop strategy) or {:error, :canceled} if the
stream has been canceled.
Use push_async/2 if you don't need backpressure feedback.
Examples
case EventStream.push(stream, {:tool_call, %{name: "read_file"}}) do
:ok -> :continue
{:error, :overflow} -> :stop_producing
end
Push an event to the stream (asynchronous, fire-and-forget).
This is a non-blocking push that ignores backpressure. Use push/2
if you need confirmation that the event was accepted.
If using :drop_oldest or :drop_newest strategies, events will be
dropped silently on overflow. With :error strategy, overflow events
are still dropped but a warning is logged.
Examples
:ok = EventStream.push_async(stream, {:agent_start})
Get the final result of the stream, blocking until complete.
Returns {:ok, messages} on successful completion or
{:error, reason, partial_state} on error/cancellation.
Options
timeout- How long to wait for completion (default::infinity)
Examples
{:ok, messages} = EventStream.result(stream)
{:ok, messages} = EventStream.result(stream, 5000)
@spec start([option()]) :: GenServer.on_start()
Start a new event stream without linking to the caller.
This is useful when the stream should outlive the calling process, such as when the caller is a short-lived runner process.
See start_link/1 for options.
@spec start_link([option()]) :: GenServer.on_start()
Start a new event stream.
Options
:owner- Process to monitor. Stream cancels if owner dies. Default:self():runner- Process producing events (e.g., CLI runner). Stream errors if runner dies. This ensures consumers are woken up if the producer crashes.:max_queue- Maximum events to buffer. Default: 10000:drop_strategy- What to do on overflow::drop_oldest,:drop_newest, or:error. Default::error:timeout- Stream timeout in milliseconds. Default: 300000ms. Set to:infinityto disable timeout.
Examples
{:ok, stream} = LemonAgent.EventStream.start_link(
owner: self(),
max_queue: 5000,
timeout: 60_000
)
@spec stats(t()) :: %{ queue_size: non_neg_integer(), max_queue: pos_integer(), dropped: non_neg_integer() }
Get current queue statistics.
Returns a map with:
:queue_size- Current number of buffered events:max_queue- Maximum queue size:dropped- Number of events dropped due to overflow
Examples
%{queue_size: 42, max_queue: 10000, dropped: 0} = EventStream.stats(stream)