ExAgent.Chunk (ExAgent v0.4.0)

Copy Markdown View Source

One event in a streamed response.

ExAgent.chat_stream/3 and ExAgent.Provider.stream/3 yield these instead of bare strings, so usage, finish reasons, reasoning traces, and tool-call deltas are all available rather than discarded.

agent
|> ExAgent.chat_stream("Explain OTP")
|> Enum.each(fn
  %ExAgent.Chunk{type: :text_delta, text: text} -> IO.write(text)
  %ExAgent.Chunk{type: :done, finish_reason: reason} -> IO.puts("\n[#{reason}]")
  _chunk -> :ok
end)

Use ExAgent.collect/1 to fold a chunk stream back into the same ExAgent.Response that ExAgent.chat/3 returns.

Types

TypeCarries
:text_delta:text - a piece of the assistant's answer
:thinking_delta:text - a piece of the model's reasoning trace
:tool_call_delta:index, :id, :name, :arguments
:usage:usage - token counts
:done:finish_reason, and :error when the stream failed

Every stream ends with exactly one :done chunk, including when it fails. That invariant is what lets ExAgent.collect/1 always return a result.

Tool-call arguments arrive in pieces

:arguments is a raw JSON fragment, not a decoded map. Providers emit function arguments split across many chunks, to be concatenated by :index before decoding. :name arrives once, on the first fragment of a call. ExAgent.collect/1 does this reassembly for you.

Summary

Functions

Builds the terminal chunk.

Builds a terminal chunk carrying an error.

Reassembles :tool_call_delta chunks into completed tool calls.

Normalizes a provider's finish-reason string.

Builds a text delta chunk.

Builds a reasoning-trace delta chunk.

Builds a usage chunk.

Types

finish_reason()

@type finish_reason() ::
  :stop | :length | :tool_calls | :content_filter | :error | nil

t()

@type t() :: %ExAgent.Chunk{
  arguments: String.t() | nil,
  error: ExAgent.Error.t() | nil,
  finish_reason: finish_reason(),
  id: String.t() | nil,
  index: non_neg_integer() | nil,
  name: String.t() | nil,
  text: String.t() | nil,
  type: type(),
  usage: map() | nil
}

type()

@type type() :: :text_delta | :thinking_delta | :tool_call_delta | :usage | :done

Functions

done(finish_reason \\ :stop, error \\ nil)

@spec done(finish_reason(), ExAgent.Error.t() | nil) :: t()

Builds the terminal chunk.

error(error)

@spec error(ExAgent.Error.t()) :: t()

Builds a terminal chunk carrying an error.

Streams never raise mid-flight: whatever was already emitted stays valid, and the failure arrives as the final chunk.

finalize_tool_calls(chunks)

@spec finalize_tool_calls([t()]) :: [map()] | nil

Reassembles :tool_call_delta chunks into completed tool calls.

Fragments are grouped by :index, concatenated in arrival order, and decoded. A fragment set that does not parse as JSON is kept as %{"raw" => fragment} rather than discarded, matching how non-streaming responses handle the same case.

Returns nil when there were no tool calls, so it can be handed straight to ExAgent.Message.new/1.

Examples

iex> chunks = [
...>   %ExAgent.Chunk{type: :tool_call_delta, index: 0, name: "sum", arguments: ~s({"a":)},
...>   %ExAgent.Chunk{type: :tool_call_delta, index: 0, arguments: ~s(1})}
...> ]
iex> ExAgent.Chunk.finalize_tool_calls(chunks)
[%{"name" => "sum", "args" => %{"a" => 1}}]

finish_reason(reason)

@spec finish_reason(String.t() | nil) :: finish_reason()

Normalizes a provider's finish-reason string.

Unrecognized values become :stop rather than being dropped, since the stream did in fact end.

Examples

iex> ExAgent.Chunk.finish_reason("tool_calls")
:tool_calls

iex> ExAgent.Chunk.finish_reason("MAX_TOKENS")
:length

text_delta(text)

@spec text_delta(String.t()) :: t()

Builds a text delta chunk.

thinking_delta(text)

@spec thinking_delta(String.t()) :: t()

Builds a reasoning-trace delta chunk.

usage(usage)

@spec usage(map()) :: t()

Builds a usage chunk.