Packages

A Gleam library for building and orchestrating agents on the BEAM.

Current section

Files

Jump to
pig src pig@obs@session.erl
Raw

src/pig@obs@session.erl

-module(pig@obs@session).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/pig/obs/session.gleam").
-export([iso_timestamp/0, format_event/1, start/1, stop/1, record/2, record_sync/2, decode_tool_call/0, decode_message/0, replay/1, start_consumer/1, supervised/2]).
-export_type([session_writer/0, writer_message/0, state/0, replay_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" JSONL session writer — records SessionEvents to a file for replay.\n"
"\n"
" OTP actor that receives SessionEvents and appends them as JSONL lines.\n"
" Two modes:\n"
" - `record()` — fire-and-forget, never blocks the agent.\n"
" - `record_sync()` — synchronous call, blocks until written. For testing.\n"
).
-opaque session_writer() :: {session_writer,
gleam@erlang@process:subject(writer_message())}.
-type writer_message() :: {write_event, pig@obs@events:session_event()} |
{write_event_sync,
pig@obs@events:session_event(),
gleam@erlang@process:subject(nil)} |
stop.
-type state() :: {state, binary()}.
-type replay_error() :: {file_error, binary()} | {parse_error, binary()}.
-file("src/pig/obs/session.gleam", 40).
?DOC(" Get current ISO 8601 timestamp.\n").
-spec iso_timestamp() -> binary().
iso_timestamp() ->
pig_obs_session_ffi:iso_timestamp().
-file("src/pig/obs/session.gleam", 618).
-spec error_to_json(pig_protocol@error:ai_error()) -> gleam@json:json().
error_to_json(Error) ->
case Error of
{api_error, Message} ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"api_error"/utf8>>)},
{<<"message"/utf8>>, gleam@json:string(Message)}]
);
rate_limited ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"rate_limited"/utf8>>)}]
);
timeout ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"timeout"/utf8>>)}]
);
{invalid_response, Detail} ->
gleam@json:object(
[{<<"type"/utf8>>,
gleam@json:string(<<"invalid_response"/utf8>>)},
{<<"detail"/utf8>>, gleam@json:string(Detail)}]
)
end.
-file("src/pig/obs/session.gleam", 641).
-spec reason_to_json(pig@obs@events:session_end_reason()) -> gleam@json:json().
reason_to_json(Reason) ->
case Reason of
normal_end ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"normal_end"/utf8>>)}]
);
{error_end, E} ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"error"/utf8>>)},
{<<"error"/utf8>>, error_to_json(E)}]
);
{max_iterations_exceeded, N} ->
gleam@json:object(
[{<<"type"/utf8>>,
gleam@json:string(<<"max_iterations_exceeded"/utf8>>)},
{<<"max_iterations"/utf8>>, gleam@json:int(N)}]
);
interrupted ->
gleam@json:object(
[{<<"type"/utf8>>, gleam@json:string(<<"interrupted"/utf8>>)}]
)
end.
-file("src/pig/obs/session.gleam", 610).
-spec tool_call_to_json(pig_protocol@message:tool_call()) -> gleam@json:json().
tool_call_to_json(Tc) ->
gleam@json:object(
[{<<"id"/utf8>>, gleam@json:string(erlang:element(2, Tc))},
{<<"name"/utf8>>, gleam@json:string(erlang:element(3, Tc))},
{<<"arguments"/utf8>>, gleam@json:string(erlang:element(4, Tc))}]
).
-file("src/pig/obs/session.gleam", 558).
-spec message_to_json(pig_protocol@message:message()) -> gleam@json:json().
message_to_json(Msg) ->
case Msg of
{user, Content} ->
gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"user"/utf8>>)},
{<<"content"/utf8>>, gleam@json:string(Content)}]
);
{system, Content@1} ->
gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"system"/utf8>>)},
{<<"content"/utf8>>, gleam@json:string(Content@1)}]
);
{assistant, Content@2, Tool_calls, Thinking, Stop_reason} ->
Base_fields = [{<<"role"/utf8>>,
gleam@json:string(<<"assistant"/utf8>>)},
{<<"content"/utf8>>, gleam@json:string(Content@2)},
{<<"tool_calls"/utf8>>,
gleam@json:array(Tool_calls, fun tool_call_to_json/1)}],
Fields_with_thinking = case Thinking of
{some, T} ->
case T of
{thinking, Content@3} ->
lists:append(
Base_fields,
[{<<"thinking"/utf8>>,
gleam@json:object(
[{<<"content"/utf8>>,
gleam@json:string(Content@3)}]
)}]
)
end;
none ->
Base_fields
end,
Fields_with_stop_reason = case Stop_reason of
{some, Sr} ->
lists:append(
Fields_with_thinking,
[{<<"stop_reason"/utf8>>,
pig_protocol@stop_reason:to_json(Sr)}]
);
none ->
Fields_with_thinking
end,
gleam@json:object(Fields_with_stop_reason);
{tool, Tool_call_id, Content@4} ->
gleam@json:object(
[{<<"role"/utf8>>, gleam@json:string(<<"tool"/utf8>>)},
{<<"tool_call_id"/utf8>>, gleam@json:string(Tool_call_id)},
{<<"content"/utf8>>, gleam@json:string(Content@4)}]
)
end.
-file("src/pig/obs/session.gleam", 664).
-spec hook_to_string(pig@obs@events:hook_point()) -> binary().
hook_to_string(Hook) ->
case Hook of
before_tool_call ->
<<"before_tool_call"/utf8>>;
after_tool_call ->
<<"after_tool_call"/utf8>>;
before_inference ->
<<"before_inference"/utf8>>;
after_inference ->
<<"after_inference"/utf8>>;
on_error ->
<<"on_error"/utf8>>;
on_complete ->
<<"on_complete"/utf8>>;
on_session_start ->
<<"on_session_start"/utf8>>;
on_session_shutdown ->
<<"on_session_shutdown"/utf8>>
end.
-file("src/pig/obs/session.gleam", 350).
?DOC(" Format a SessionEvent as a JSON string (pure function, no side effects).\n").
-spec format_event(pig@obs@events:session_event()) -> binary().
format_event(Event) ->
Ts = iso_timestamp(),
case Event of
{session_started,
Agent_id,
Agent_name,
Model,
Provider_name,
System_prompt} ->
Fields = [{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"session_started"/utf8>>)},
{<<"model"/utf8>>, gleam@json:string(Model)}],
With_agent_id = case Agent_id of
{some, V} ->
lists:append(
Fields,
[{<<"agent_id"/utf8>>, gleam@json:string(V)}]
);
none ->
Fields
end,
With_agent_name = case Agent_name of
{some, V@1} ->
lists:append(
With_agent_id,
[{<<"agent_name"/utf8>>, gleam@json:string(V@1)}]
);
none ->
With_agent_id
end,
With_provider = case Provider_name of
{some, V@2} ->
lists:append(
With_agent_name,
[{<<"provider_name"/utf8>>, gleam@json:string(V@2)}]
);
none ->
With_agent_name
end,
With_system = case System_prompt of
{some, V@3} ->
lists:append(
With_provider,
[{<<"system_prompt"/utf8>>, gleam@json:string(V@3)}]
);
none ->
With_provider
end,
_pipe = gleam@json:object(With_system),
gleam@json:to_string(_pipe);
{inference_started, Model@1, Message_count} ->
_pipe@1 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"inference_started"/utf8>>)},
{<<"model"/utf8>>, gleam@json:string(Model@1)},
{<<"message_count"/utf8>>, gleam@json:int(Message_count)}]
),
gleam@json:to_string(_pipe@1);
{inference_completed,
Message,
Response_id,
Response_model,
Stop_reason,
Input_tokens,
Output_tokens,
Duration_ms,
Input_messages} ->
Fields@1 = [{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"inference_completed"/utf8>>)},
{<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms)},
{<<"message"/utf8>>, message_to_json(Message)},
{<<"input_messages"/utf8>>,
gleam@json:array(Input_messages, fun message_to_json/1)}],
With_response_id = case Response_id of
{some, V@4} ->
lists:append(
Fields@1,
[{<<"response_id"/utf8>>, gleam@json:string(V@4)}]
);
none ->
Fields@1
end,
With_response_model = case Response_model of
{some, V@5} ->
lists:append(
With_response_id,
[{<<"response_model"/utf8>>, gleam@json:string(V@5)}]
);
none ->
With_response_id
end,
With_stop_reason = case Stop_reason of
{some, V@6} ->
lists:append(
With_response_model,
[{<<"stop_reason"/utf8>>,
pig_protocol@stop_reason:to_json(V@6)}]
);
none ->
With_response_model
end,
With_input_tokens = case Input_tokens of
{some, V@7} ->
lists:append(
With_stop_reason,
[{<<"input_tokens"/utf8>>, gleam@json:int(V@7)}]
);
none ->
With_stop_reason
end,
With_output_tokens = case Output_tokens of
{some, V@8} ->
lists:append(
With_input_tokens,
[{<<"output_tokens"/utf8>>, gleam@json:int(V@8)}]
);
none ->
With_input_tokens
end,
_pipe@2 = gleam@json:object(With_output_tokens),
gleam@json:to_string(_pipe@2);
{tool_started, Tool_call} ->
_pipe@3 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"tool_started"/utf8>>)},
{<<"tool_call"/utf8>>, tool_call_to_json(Tool_call)}]
),
gleam@json:to_string(_pipe@3);
{tool_executed, Tool_call@1, Result, Duration_ms@1} ->
_pipe@4 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"tool_executed"/utf8>>)},
{<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms@1)},
{<<"tool_call"/utf8>>, tool_call_to_json(Tool_call@1)},
{<<"result"/utf8>>, gleam@json:string(Result)}]
),
gleam@json:to_string(_pipe@4);
{tool_blocked, Tool_call@2, Hook_name, Reason} ->
_pipe@5 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"tool_blocked"/utf8>>)},
{<<"tool_call"/utf8>>, tool_call_to_json(Tool_call@2)},
{<<"hook_name"/utf8>>, gleam@json:string(Hook_name)},
{<<"reason"/utf8>>, gleam@json:string(Reason)}]
),
gleam@json:to_string(_pipe@5);
{hook_acted, Hook_name@1, Hook_point, Action} ->
_pipe@6 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>, gleam@json:string(<<"hook_acted"/utf8>>)},
{<<"hook_name"/utf8>>, gleam@json:string(Hook_name@1)},
{<<"hook_point"/utf8>>,
gleam@json:string(hook_to_string(Hook_point))},
{<<"action"/utf8>>,
gleam@json:object(
[{<<"action_type"/utf8>>,
gleam@json:string(erlang:element(2, Action))},
{<<"description"/utf8>>,
gleam@json:string(erlang:element(3, Action))}]
)}]
),
gleam@json:to_string(_pipe@6);
{inference_failed, Error, Duration_ms@2, Input_messages@1} ->
_pipe@7 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"inference_failed"/utf8>>)},
{<<"duration_ms"/utf8>>, gleam@json:int(Duration_ms@2)},
{<<"error"/utf8>>, error_to_json(Error)},
{<<"input_messages"/utf8>>,
gleam@json:array(
Input_messages@1,
fun message_to_json/1
)}]
),
gleam@json:to_string(_pipe@7);
{session_ended, Reason@1} ->
_pipe@8 = gleam@json:object(
[{<<"ts"/utf8>>, gleam@json:string(Ts)},
{<<"event"/utf8>>,
gleam@json:string(<<"session_ended"/utf8>>)},
{<<"reason"/utf8>>, reason_to_json(Reason@1)}]
),
gleam@json:to_string(_pipe@8)
end.
-file("src/pig/obs/session.gleam", 517).
-spec handle_message(state(), writer_message()) -> gleam@otp@actor:next(state(), writer_message()).
handle_message(State, Message) ->
case Message of
{write_event, Event} ->
Json_str = format_event(Event),
_ = simplifile:append(
erlang:element(2, State),
<<Json_str/binary, "\n"/utf8>>
),
gleam@otp@actor:continue(State);
{write_event_sync, Event@1, Reply_subject} ->
Json_str@1 = format_event(Event@1),
_ = simplifile:append(
erlang:element(2, State),
<<Json_str@1/binary, "\n"/utf8>>
),
gleam@erlang@process:send(Reply_subject, nil),
gleam@otp@actor:continue(State);
stop ->
gleam@otp@actor:stop()
end.
-file("src/pig/obs/session.gleam", 66).
?DOC(" Start a new session writer actor that appends events to the given file.\n").
-spec start(binary()) -> {ok, session_writer()} |
{error, gleam@otp@actor:start_error()}.
start(Path) ->
Builder = begin
_pipe = gleam@otp@actor:new({state, Path}),
gleam@otp@actor:on_message(_pipe, fun handle_message/2)
end,
case gleam@otp@actor:start(Builder) of
{ok, Started} ->
{ok, {session_writer, erlang:element(3, Started)}};
{error, E} ->
{error, E}
end.
-file("src/pig/obs/session.gleam", 77).
?DOC(" Stop the session writer actor.\n").
-spec stop(session_writer()) -> nil.
stop(Writer) ->
{session_writer, Subject} = Writer,
gleam@erlang@process:send(Subject, stop).
-file("src/pig/obs/session.gleam", 83).
?DOC(" Record a session event. Fire-and-forget: does not block.\n").
-spec record(session_writer(), pig@obs@events:session_event()) -> nil.
record(Writer, Event) ->
{session_writer, Subject} = Writer,
gleam@erlang@process:send(Subject, {write_event, Event}).
-file("src/pig/obs/session.gleam", 90).
?DOC(
" Record a session event synchronously. Blocks until the event is\n"
" written to disk. Use this in tests for deterministic assertions.\n"
).
-spec record_sync(session_writer(), pig@obs@events:session_event()) -> nil.
record_sync(Writer, Event) ->
{session_writer, Subject} = Writer,
Reply_subject = gleam@erlang@process:new_subject(),
gleam@erlang@process:send(Subject, {write_event_sync, Event, Reply_subject}),
case gleam@erlang@process:'receive'(Reply_subject, 5000) of
{ok, _} -> nil;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"pig/obs/session"/utf8>>,
function => <<"record_sync"/utf8>>,
line => 94,
value => _assert_fail,
start => 3452,
'end' => 3507,
pattern_start => 3463,
pattern_end => 3468})
end,
nil.
-file("src/pig/obs/session.gleam", 226).
?DOC(" Parse a Tool message from a ToolBlocked JSON line.\n").
-spec parse_blocked_tool_message(binary()) -> {ok,
pig_protocol@message:message()} |
{error, nil}.
parse_blocked_tool_message(Line) ->
Id_decoder = gleam@dynamic@decode:at(
[<<"tool_call"/utf8>>, <<"id"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
Hook_name_decoder = gleam@dynamic@decode:at(
[<<"hook_name"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
Reason_decoder = gleam@dynamic@decode:at(
[<<"reason"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
case gleam@json:parse(Line, Id_decoder) of
{ok, Id} ->
case {gleam@json:parse(Line, Hook_name_decoder),
gleam@json:parse(Line, Reason_decoder)} of
{{ok, Hook_name}, {ok, Reason}} ->
{ok,
{tool,
Id,
<<<<<<"Tool blocked by '"/utf8, Hook_name/binary>>/binary,
"': "/utf8>>/binary,
Reason/binary>>}};
{_, _} ->
{error, nil}
end;
{error, _} ->
{error, nil}
end.
-file("src/pig/obs/session.gleam", 210).
?DOC(" Parse a Tool message from a ToolExecuted JSON line.\n").
-spec parse_tool_message(binary()) -> {ok, pig_protocol@message:message()} |
{error, nil}.
parse_tool_message(Line) ->
Id_decoder = gleam@dynamic@decode:at(
[<<"tool_call"/utf8>>, <<"id"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
Result_decoder = gleam@dynamic@decode:at(
[<<"result"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
case gleam@json:parse(Line, Id_decoder) of
{ok, Id} ->
case gleam@json:parse(Line, Result_decoder) of
{ok, Result} ->
{ok, {tool, Id, Result}};
{error, _} ->
{error, nil}
end;
{error, _} ->
{error, nil}
end.
-file("src/pig/obs/session.gleam", 182).
?DOC(" Decode the event type from a JSON line.\n").
-spec decode_event_type_str(binary()) -> binary().
decode_event_type_str(Line) ->
Decoder = gleam@dynamic@decode:at(
[<<"event"/utf8>>],
{decoder, fun gleam@dynamic@decode:decode_string/1}
),
case gleam@json:parse(Line, Decoder) of
{ok, Event_type} ->
Event_type;
{error, _} ->
<<""/utf8>>
end.
-file("src/pig/obs/session.gleam", 169).
?DOC(" Get lines after the given line.\n").
-spec lines_after(list(binary()), binary()) -> list(binary()).
lines_after(Lines, Target) ->
case Lines of
[] ->
[];
[L | Rest] ->
case L =:= Target of
true ->
Rest;
false ->
lines_after(Rest, Target)
end
end.
-file("src/pig/obs/session.gleam", 302).
?DOC(" Decode Thinking from JSON.\n").
-spec decode_thinking() -> gleam@dynamic@decode:decoder(gleam@option:option(pig_protocol@message:thinking())).
decode_thinking() ->
gleam@dynamic@decode:field(
<<"content"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Content) ->
gleam@dynamic@decode:success({some, {thinking, Content}})
end
).
-file("src/pig/obs/session.gleam", 308).
?DOC(" Decode a ToolCall from JSON.\n").
-spec decode_tool_call() -> gleam@dynamic@decode:decoder(pig_protocol@message:tool_call()).
decode_tool_call() ->
gleam@dynamic@decode:field(
<<"id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Id) ->
gleam@dynamic@decode:field(
<<"name"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Name) ->
gleam@dynamic@decode:field(
<<"arguments"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Arguments_json) ->
gleam@dynamic@decode:success(
{tool_call, Id, Name, Arguments_json}
)
end
)
end
)
end
).
-file("src/pig/obs/session.gleam", 251).
?DOC(" Decode a Message from JSON.\n").
-spec decode_message() -> gleam@dynamic@decode:decoder(pig_protocol@message:message()).
decode_message() ->
gleam@dynamic@decode:field(
<<"role"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Role) -> case Role of
<<"user"/utf8>> ->
gleam@dynamic@decode:field(
<<"content"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Content) ->
gleam@dynamic@decode:success({user, Content})
end
);
<<"system"/utf8>> ->
gleam@dynamic@decode:field(
<<"content"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Content@1) ->
gleam@dynamic@decode:success({system, Content@1})
end
);
<<"tool"/utf8>> ->
gleam@dynamic@decode:field(
<<"tool_call_id"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Tool_call_id) ->
gleam@dynamic@decode:field(
<<"content"/utf8>>,
{decoder,
fun gleam@dynamic@decode:decode_string/1},
fun(Content@2) ->
gleam@dynamic@decode:success(
{tool, Tool_call_id, Content@2}
)
end
)
end
);
<<"assistant"/utf8>> ->
gleam@dynamic@decode:field(
<<"content"/utf8>>,
{decoder, fun gleam@dynamic@decode:decode_string/1},
fun(Content@3) ->
gleam@dynamic@decode:field(
<<"tool_calls"/utf8>>,
gleam@dynamic@decode:list(decode_tool_call()),
fun(Tool_calls) ->
gleam@dynamic@decode:optional_field(
<<"thinking"/utf8>>,
none,
decode_thinking(),
fun(Thinking) ->
gleam@dynamic@decode:optional_field(
<<"stop_reason"/utf8>>,
none,
gleam@dynamic@decode:optional(
pig_protocol@stop_reason:decoder(
)
),
fun(Stop_reason) ->
gleam@dynamic@decode:success(
{assistant,
Content@3,
Tool_calls,
Thinking,
Stop_reason}
)
end
)
end
)
end
)
end
);
_ ->
gleam@dynamic@decode:failure(
{user, <<""/utf8>>},
<<"unknown message role: "/utf8, Role/binary>>
)
end end
).
-file("src/pig/obs/session.gleam", 191).
?DOC(" Parse input_messages and message from an InferenceCompleted JSON line.\n").
-spec parse_inference_messages(binary()) -> {ok,
list(pig_protocol@message:message())} |
{error, replay_error()}.
parse_inference_messages(Line) ->
Input_decoder = gleam@dynamic@decode:at(
[<<"input_messages"/utf8>>],
gleam@dynamic@decode:list(decode_message())
),
case gleam@json:parse(Line, Input_decoder) of
{ok, Input_msgs} ->
Msg_decoder = gleam@dynamic@decode:at(
[<<"message"/utf8>>],
decode_message()
),
case gleam@json:parse(Line, Msg_decoder) of
{ok, Response_msg} ->
{ok, lists:append(Input_msgs, [Response_msg])};
{error, _} ->
{error,
{parse_error,
<<"Failed to parse message: "/utf8, Line/binary>>}}
end;
{error, _} ->
{error,
{parse_error,
<<"Failed to parse input_messages: "/utf8, Line/binary>>}}
end.
-file("src/pig/obs/session.gleam", 158).
?DOC(" Find the last InferenceCompleted line.\n").
-spec find_last_inference_completed(list(binary())) -> gleam@option:option(binary()).
find_last_inference_completed(Lines) ->
_pipe = Lines,
gleam@list:fold(
_pipe,
none,
fun(Acc, Line) -> case decode_event_type_str(Line) of
<<"inference_completed"/utf8>> ->
{some, Line};
_ ->
Acc
end end
).
-file("src/pig/obs/session.gleam", 128).
?DOC(" Replay from a list of JSONL lines.\n").
-spec replay_lines(list(binary())) -> {ok, list(pig_protocol@message:message())} |
{error, replay_error()}.
replay_lines(Lines) ->
Last_inference = find_last_inference_completed(Lines),
case Last_inference of
none ->
{ok, []};
{some, Input_messages_json} ->
case parse_inference_messages(Input_messages_json) of
{ok, Messages} ->
Remaining = lines_after(Lines, Input_messages_json),
Tool_msgs = begin
_pipe = Remaining,
gleam@list:filter_map(
_pipe,
fun(Line) -> case decode_event_type_str(Line) of
<<"tool_executed"/utf8>> ->
parse_tool_message(Line);
<<"tool_blocked"/utf8>> ->
parse_blocked_tool_message(Line);
_ ->
{error, nil}
end end
)
end,
{ok, lists:append(Messages, Tool_msgs)};
{error, E} ->
{error, E}
end
end.
-file("src/pig/obs/session.gleam", 111).
?DOC(
" Replay a JSONL session file and reconstruct the message history.\n"
"\n"
" Strategy: find the last InferenceCompleted event. Its input_messages +\n"
" message give the complete history. For partial sessions (crash mid-loop),\n"
" also include Tool messages from ToolExecuted events after the last inference.\n"
).
-spec replay(binary()) -> {ok, list(pig_protocol@message:message())} |
{error, replay_error()}.
replay(Path) ->
case simplifile:read(Path) of
{error, E} ->
{error, {file_error, gleam@string:inspect(E)}};
{ok, Content} ->
Lines = begin
_pipe = Content,
_pipe@1 = gleam@string:split(_pipe, <<"\n"/utf8>>),
gleam@list:filter(_pipe@1, fun(L) -> L /= <<""/utf8>> end)
end,
case Lines of
[] ->
{ok, []};
_ ->
replay_lines(Lines)
end
end.
-file("src/pig/obs/session.gleam", 541).
?DOC(
" Handle consumer messages (SessionEvent directly, not wrapped in WriterMessage).\n"
" Used by the supervised consumer actor that receives events from the dispatcher.\n"
).
-spec handle_consumer_message(state(), pig@obs@events:session_event()) -> gleam@otp@actor:next(state(), pig@obs@events:session_event()).
handle_consumer_message(State, Event) ->
Json_str = format_event(Event),
case simplifile:append(
erlang:element(2, State),
<<Json_str/binary, "\n"/utf8>>
) of
{ok, _} ->
gleam@otp@actor:continue(State);
{error, _} ->
gleam@otp@actor:stop()
end.
-file("src/pig/obs/session.gleam", 319).
?DOC(
" Start a session consumer actor that accepts SessionEvent directly.\n"
" Used by the dispatcher to fan out events. Returns the Subject for registration.\n"
" This is the consumer version of the actor — it receives SessionEvent directly,\n"
" not WriterMessage wrappers. Fire-and-forget: does not block.\n"
).
-spec start_consumer(binary()) -> {ok,
gleam@erlang@process:subject(pig@obs@events:session_event())} |
{error, gleam@otp@actor:start_error()}.
start_consumer(Path) ->
Builder = begin
_pipe = gleam@otp@actor:new({state, Path}),
gleam@otp@actor:on_message(_pipe, fun handle_consumer_message/2)
end,
case gleam@otp@actor:start(Builder) of
{ok, Started} ->
{ok, erlang:element(3, Started)};
{error, E} ->
{error, E}
end.
-file("src/pig/obs/session.gleam", 333).
?DOC(
" Create a supervised session consumer actor for use in a supervision tree.\n"
" The supervised actor's message type is SessionEvent directly (not WriterMessage).\n"
).
-spec supervised(
binary(),
gleam@erlang@process:name(pig@obs@events:session_event())
) -> gleam@otp@supervision:child_specification(nil).
supervised(Path, Name) ->
gleam@otp@supervision:worker(
fun() ->
Builder = begin
_pipe = gleam@otp@actor:new({state, Path}),
_pipe@1 = gleam@otp@actor:on_message(
_pipe,
fun handle_consumer_message/2
),
gleam@otp@actor:named(_pipe@1, Name)
end,
case gleam@otp@actor:start(Builder) of
{ok, Started} ->
{ok, {started, erlang:element(2, Started), nil}};
{error, E} ->
{error, E}
end
end
).