Packages

AI agent framework for Elixir built on OTP. TEA-based agents with crash isolation, inter-agent messaging, team supervision, and real SSE streaming to Anthropic, OpenAI, Ollama, and more.

Current section

Files

Jump to
raxol_agent lib raxol agent stream event.ex
Raw

lib/raxol/agent/stream/event.ex

defmodule Raxol.Agent.Stream.Event do
@moduledoc """
Typed structs for `Raxol.Agent.Stream` events plus conversion helpers.
`Stream.run/2` and `Stream.react/2` emit events as bare tuples for
pattern-match ergonomics and backward compatibility:
{:text_delta, "hello"}
{:tool_use, %{name: "linear_graphql", arguments: %{}, id: "call_1"}}
{:tool_result, %{name: "linear_graphql", result: %{...}}}
{:turn_complete, %{content: "...", usage: %{}, iteration: 1}}
{:done, %{content: "...", tool_results: [...], usage: %{}}}
{:error, reason}
Consumers that want stronger typing -- e.g., orchestrators forwarding
events to surfaces -- can use `from_tuple/1` to lift each tuple into
a typed struct, and `to_payload/1` to project it down to a generic
map suitable for telemetry or pubsub.
Both helpers are pure; they do not subscribe to or transform the
underlying stream.
## Example
stream = Raxol.Agent.Stream.run(prompt, opts)
Enum.each(stream, fn event ->
typed = Raxol.Agent.Stream.Event.from_tuple(event)
send(parent, {:run_event, key, Raxol.Agent.Stream.Event.to_payload(typed)})
end)
See `Raxol.Agent.EventForwarder` for the common "send to parent" pattern
packaged as a single call.
"""
defmodule TextDelta do
@moduledoc "Streaming text chunk from the LLM."
@enforce_keys [:text]
defstruct [:text]
@type t :: %__MODULE__{text: String.t()}
end
defmodule ToolUse do
@moduledoc "LLM is requesting a tool call."
@enforce_keys [:name]
defstruct [:name, :arguments, :id]
@type t :: %__MODULE__{
name: String.t(),
arguments: map(),
id: String.t() | nil
}
end
defmodule ToolResult do
@moduledoc "Result of executing a tool."
@enforce_keys [:name, :result]
defstruct [:name, :result]
@type t :: %__MODULE__{name: String.t(), result: term()}
end
defmodule TurnComplete do
@moduledoc "End of one ReAct turn."
@enforce_keys [:content]
defstruct [:content, :usage, :iteration]
@type t :: %__MODULE__{
content: String.t(),
usage: map(),
iteration: non_neg_integer()
}
end
defmodule Done do
@moduledoc "Final answer from the agent."
@enforce_keys [:content]
defstruct [:content, :tool_results, :usage]
@type t :: %__MODULE__{
content: String.t(),
tool_results: list(),
usage: map()
}
end
defmodule Error do
@moduledoc "Error during stream execution."
@enforce_keys [:reason]
defstruct [:reason]
@type t :: %__MODULE__{reason: term()}
end
@type t ::
TextDelta.t()
| ToolUse.t()
| ToolResult.t()
| TurnComplete.t()
| Done.t()
| Error.t()
@type tuple_event :: Raxol.Agent.Stream.event()
@doc """
Lifts a stream tuple into the corresponding typed struct.
Unknown shapes return an `Error` struct rather than raising, so a single
bad event never aborts a forwarding pipeline.
"""
@spec from_tuple(tuple_event() | term()) :: t()
def from_tuple({:text_delta, text}) when is_binary(text),
do: %TextDelta{text: text}
def from_tuple({:tool_use, %{name: name} = info}) when is_binary(name) do
%ToolUse{
name: name,
arguments: Map.get(info, :arguments, %{}),
id: Map.get(info, :id)
}
end
def from_tuple({:tool_result, %{name: name, result: result}})
when is_binary(name),
do: %ToolResult{name: name, result: result}
def from_tuple({:turn_complete, %{} = info}) do
%TurnComplete{
content: Map.get(info, :content, ""),
usage: Map.get(info, :usage, %{}),
iteration: Map.get(info, :iteration, 0)
}
end
def from_tuple({:done, %{} = info}) do
%Done{
content: Map.get(info, :content, ""),
tool_results: Map.get(info, :tool_results, []),
usage: Map.get(info, :usage, %{})
}
end
def from_tuple({:error, reason}), do: %Error{reason: reason}
def from_tuple(other), do: %Error{reason: {:unknown_event, other}}
@doc """
Projects an event (tuple or struct) onto a generic map suitable for
telemetry, pubsub, or surface forwarding.
All payloads carry `:event` (atom name), `:timestamp`, and a `:message`
string. Specific events add `:usage` (TurnComplete, Done) or `:payload`
(ToolUse, ToolResult) when present. The shape mirrors what
`Raxol.Symphony.Orchestrator` already integrates so existing surface
consumers don't need to change.
"""
@spec to_payload(t() | tuple_event()) :: map()
def to_payload(%TextDelta{text: text}),
do: base(:text_delta, text)
def to_payload(%ToolUse{name: name} = ev) do
base(:tool_use, "tool: #{name}")
|> Map.put(:payload, %{name: ev.name, arguments: ev.arguments, id: ev.id})
end
def to_payload(%ToolResult{name: name} = ev) do
base(:tool_result, "result: #{name}")
|> Map.put(:payload, %{name: ev.name, result: ev.result})
end
def to_payload(%TurnComplete{} = ev) do
base(:turn_completed, "turn complete")
|> Map.put(:usage, ev.usage)
end
def to_payload(%Done{} = ev) do
base(:turn_completed, ev.content || "done")
|> Map.put(:usage, ev.usage)
end
def to_payload(%Error{reason: reason}),
do: base(:turn_failed, inspect(reason))
def to_payload(tuple) when is_tuple(tuple),
do: tuple |> from_tuple() |> to_payload()
defp base(event, message) do
%{
event: event,
message: message,
timestamp: DateTime.utc_now()
}
end
end