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
Current section
Files
lib/raxol/agent/harness/stream_json.ex
defmodule Raxol.Agent.Harness.StreamJson do
@moduledoc """
Parser for the `--output-format stream-json` NDJSON protocol shared by Claude
Code and Cursor's `cursor-agent`.
Each stdout line is a standalone JSON object tagged with a `type`:
- `system` (init/config) -- ignored.
- `assistant` -- a model message; its `content` blocks become `:text`,
`:reasoning` (thinking), or `:tool_call` events.
- `user` -- tool results echoed back; ignored (the MCP server owns execution).
- `result` -- the terminal event; `success` yields `{:done, ...}` with the
final text + usage, an error subtype yields `{:error, ...}`.
Non-JSON lines (banners, warnings) parse to `[]` so stray output never breaks a
run. Returns `Raxol.Agent.NativeHarness` event tuples.
"""
alias Raxol.Agent.NativeHarness
@doc "Parse one NDJSON line into normalized harness events."
@spec parse_line(String.t()) :: [NativeHarness.event()]
def parse_line(line) do
case Jason.decode(String.trim(line)) do
{:ok, %{} = obj} -> parse_object(obj)
_ -> []
end
end
defp parse_object(%{"type" => "assistant", "message" => %{"content" => content}})
when is_list(content) do
Enum.flat_map(content, &parse_block/1)
end
defp parse_object(%{"type" => "result", "subtype" => "success"} = obj) do
[{:done, %{content: result_text(obj), usage: usage(obj)}}]
end
defp parse_object(%{"type" => "result", "subtype" => subtype} = obj) do
[{:error, {:result_error, subtype, result_text(obj)}}]
end
defp parse_object(_other), do: []
defp parse_block(%{"type" => "text", "text" => text}) when is_binary(text),
do: [{:text, text}]
defp parse_block(%{"type" => "thinking", "thinking" => text}) when is_binary(text),
do: [{:reasoning, text}]
defp parse_block(%{"type" => "tool_use", "name" => name} = block) do
[{:tool_call, %{name: name, input: Map.get(block, "input", %{}), id: Map.get(block, "id")}}]
end
defp parse_block(_other), do: []
defp result_text(obj) do
case Map.get(obj, "result") do
text when is_binary(text) -> text
_ -> ""
end
end
defp usage(obj) do
case Map.get(obj, "usage") do
%{} = usage -> usage
_ -> %{}
end
end
end