-module(pig). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/pig.gleam"). -export([new/1, with_tool/2, with_tools/2, with_skill/2, with_hooks/2, with_system_prompt/2, with_model/2, with_agent_name/2, with_agent_id/2, with_agent_description/2, with_agent_version/2, with_provider_name/2, with_session_writer/2, with_session_store/2, with_terminal_output/1, add_consumer/2, with_consumer_specs/2, with_initial_history/2, agent_config/1, build_agent_config/1, start/1, run_with_timeout/3, run/2, try_run_with_timeout/3, run_continue_with_timeout/2, try_run_continue_with_timeout/2, run_continue/1, stop/1, history/1, test_harness/0]). -export_type([pig_config/0, loaded_session/0, agent/0, start_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( " Top-level public API for the pig library.\n" "\n" " Builder pattern: `new(provider) |> with_tool(t) |> with_skill(s) |> start`\n" " Then: `run(agent, prompt)` or `run_with_timeout(agent, prompt, ms)`\n" "\n" " Thin public surface. All logic in agent/update (pure core) +\n" " agent/runtime (impure interpreter).\n" ). -opaque pig_config() :: {pig_config, pig@agent@state:agent_config(), list(pig@skill:skill()), list(pig@obs@consumer_spec:consumer_spec()), list(pig@hooks:hooks()), list(pig_protocol@message:message()), gleam@option:option(pig@session_store:session_store())}. -type loaded_session() :: {loaded_session, pig@session_store:session_store(), pig@session_store:session()}. -opaque agent() :: {agent, gleam@erlang@process:subject(pig@agent@runtime:runtime_msg())}. -type start_error() :: {invalid_configuration, binary()} | {actor_start, gleam@otp@actor:start_error()} | {session_load, pig@session_store:session_error()}. -file("src/pig.gleam", 67). ?DOC( " Create a new PigConfig with a provider and sensible defaults.\n" "\n" " Defaults: empty tool registry, no system prompt, no skills,\n" " no persistence, model \"unknown\", max iterations 50, no consumers.\n" ). -spec new( fun((list(pig_protocol@message:message()), list(pig_protocol@tool_definition:tool_definition())) -> {ok, pig_protocol@inference:inference_result()} | {error, pig_protocol@error:ai_error()}) ) -> pig_config(). new(Provider) -> {pig_config, pig@agent@state:config(Provider), [], [], [], [], none}. -file("src/pig.gleam", 79). ?DOC(" Register a tool in the config.\n"). -spec with_tool(pig_config(), pig@tool:tool()) -> pig_config(). with_tool(Config, T) -> Updated_registry = pig@tool:register( erlang:element(3, erlang:element(2, Config)), T ), {pig_config, begin _record = erlang:element(2, Config), {agent_config, erlang:element(2, _record), Updated_registry, erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record), erlang:element(10, _record), erlang:element(11, _record), erlang:element(12, _record)} end, erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 91). ?DOC(" Register multiple tools in the config.\n"). -spec with_tools(pig_config(), list(pig@tool:tool())) -> pig_config(). with_tools(Config, Tools) -> gleam@list:fold(Tools, Config, fun with_tool/2). -file("src/pig.gleam", 100). ?DOC( " Add a skill and register the librarian tool.\n" "\n" " Skills are accumulated. On `start`, a single librarian tool is\n" " created from all skills, and skill descriptions are injected\n" " into the system prompt.\n" ). -spec with_skill(pig_config(), pig@skill:skill()) -> pig_config(). with_skill(Config, S) -> {pig_config, erlang:element(2, Config), [S | erlang:element(3, Config)], erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 105). ?DOC(" Register a hooks set for lifecycle mediation.\n"). -spec with_hooks(pig_config(), pig@hooks:hooks()) -> pig_config(). with_hooks(Config, H) -> {pig_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), lists:append(erlang:element(5, Config), [H]), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 110). ?DOC(" Set the system prompt.\n"). -spec with_system_prompt(pig_config(), binary()) -> pig_config(). with_system_prompt(Config, Prompt) -> {pig_config, pig@agent@state:with_system_prompt(erlang:element(2, Config), Prompt), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 118). ?DOC(" Set the model name for telemetry and logging.\n"). -spec with_model(pig_config(), binary()) -> pig_config(). with_model(Config, Model) -> {pig_config, pig@agent@state:with_model(erlang:element(2, Config), Model), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 126). ?DOC(" Set the agent name.\n"). -spec with_agent_name(pig_config(), binary()) -> pig_config(). with_agent_name(Config, Name) -> {pig_config, pig@agent@state:with_agent_name(erlang:element(2, Config), Name), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 134). ?DOC(" Set the agent ID.\n"). -spec with_agent_id(pig_config(), binary()) -> pig_config(). with_agent_id(Config, Id) -> {pig_config, pig@agent@state:with_agent_id(erlang:element(2, Config), Id), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 142). ?DOC(" Set the agent description.\n"). -spec with_agent_description(pig_config(), binary()) -> pig_config(). with_agent_description(Config, Description) -> {pig_config, pig@agent@state:with_agent_description( erlang:element(2, Config), Description ), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 153). ?DOC(" Set the agent version.\n"). -spec with_agent_version(pig_config(), binary()) -> pig_config(). with_agent_version(Config, Version) -> {pig_config, pig@agent@state:with_agent_version(erlang:element(2, Config), Version), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 161). ?DOC(" Set the provider name.\n"). -spec with_provider_name(pig_config(), binary()) -> pig_config(). with_provider_name(Config, Name) -> {pig_config, pig@agent@state:with_provider_name(erlang:element(2, Config), Name), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 170). ?DOC( " Register a session writer consumer that writes JSONL to the given path.\n" " Also sets session_path on agent config for replay on init.\n" ). -spec with_session_writer(pig_config(), binary()) -> pig_config(). with_session_writer(Config, Path) -> Name = gleam_erlang_ffi:new_name(<<"pig_session_writer"/utf8>>), Spec = pig@obs@session:supervised(Path, Name), Start_fn = fun() -> pig@obs@session:start_consumer(Path) end, {pig_config, pig@agent@state:with_session_path(erlang:element(2, Config), Path), erlang:element(3, Config), [{consumer_spec, Spec, Name, Start_fn} | erlang:element(4, Config)], erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 188). ?DOC( " Configure synchronous durable storage for accepted agent transitions.\n" "\n" " The store is loaded before the agent starts. Each newly accepted message\n" " delta is committed before Pig executes its effects or returns success.\n" ). -spec with_session_store(pig_config(), pig@session_store:session_store()) -> pig_config(). with_session_store(Config, Store) -> {pig_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), {some, Store}}. -file("src/pig.gleam", 193). ?DOC(" Register a terminal output consumer that prints formatted events to stdout.\n"). -spec with_terminal_output(pig_config()) -> pig_config(). with_terminal_output(Config) -> Name = gleam_erlang_ffi:new_name(<<"pig_terminal"/utf8>>), Spec = pig@obs@terminal:supervised(Name), Start_fn = fun() -> pig@obs@terminal:start_consumer() end, {pig_config, erlang:element(2, Config), erlang:element(3, Config), [{consumer_spec, Spec, Name, Start_fn} | erlang:element(4, Config)], erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 212). ?DOC( " Register a custom consumer specification.\n" "\n" " Appends to the list of consumer specs. Consumers receive pig's\n" " `SessionEvent` stream — useful for bridging events into an external\n" " store (e.g. a host runtime's observability table keyed by `run_id`).\n" "\n" " This is the seam host runtimes use to capture pig's agent-internal\n" " events (token usage, tool calls, inference timing) without pig ever\n" " depending on them.\n" ). -spec add_consumer(pig_config(), pig@obs@consumer_spec:consumer_spec()) -> pig_config(). add_consumer(Config, Spec) -> {pig_config, erlang:element(2, Config), erlang:element(3, Config), lists:append(erlang:element(4, Config), [Spec]), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 224). ?DOC( " Replace the list of consumer specifications.\n" "\n" " Overwrites any previously registered consumers (including those added\n" " by `with_session_writer` / `with_terminal_output` / `add_consumer`).\n" " Pass an empty list to clear all consumers.\n" ). -spec with_consumer_specs( pig_config(), list(pig@obs@consumer_spec:consumer_spec()) ) -> pig_config(). with_consumer_specs(Config, Specs) -> {pig_config, erlang:element(2, Config), erlang:element(3, Config), Specs, erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config)}. -file("src/pig.gleam", 244). ?DOC( " Seed the conversation with initial messages.\n" "\n" " Messages are appended to the agent's history after session replay\n" " (if any) when the agent starts via `start()`. This allows resuming\n" " a previous conversation or providing context before the first prompt.\n" "\n" " A non-empty initial history cannot be combined with `with_session_store`:\n" " a configured durable store is authoritative, so `start` rejects that\n" " configuration. Persist the history in the store instead.\n" "\n" " The provider will see these messages on the first `run()` call,\n" " along with any messages accumulated from session replay and the\n" " new user prompt.\n" ). -spec with_initial_history(pig_config(), list(pig_protocol@message:message())) -> pig_config(). with_initial_history(Config, Messages) -> {pig_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), Messages, erlang:element(7, Config)}. -file("src/pig.gleam", 252). ?DOC(" Get the underlying AgentConfig. Useful for testing and inspection.\n"). -spec agent_config(pig_config()) -> pig@agent@state:agent_config(). agent_config(Config) -> erlang:element(2, Config). -file("src/pig.gleam", 488). ?DOC( " Build the final AgentConfig from a PigConfig.\n" "\n" " Registers librarian tool if skills are present and composes\n" " system prompt from skill descriptions and tool info. Used by\n" " `start` and `pig/supervisor.start_supervised`.\n" ). -spec build_agent_config(pig_config()) -> pig@agent@state:agent_config(). build_agent_config(Config) -> Config_with_librarian = case erlang:element(3, Config) of [] -> erlang:element(2, Config); Skills -> Librarian_tool = pig@skill@librarian:librarian_tool(Skills), _record = erlang:element(2, Config), {agent_config, erlang:element(2, _record), pig@tool:register( erlang:element(3, erlang:element(2, Config)), Librarian_tool ), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record), erlang:element(10, _record), erlang:element(11, _record), erlang:element(12, _record)} end, Fragments = [], Fragments@1 = case erlang:element(3, Config) of [] -> Fragments; Skills@1 -> Skill_fragment = begin _pipe = Skills@1, _pipe@1 = gleam@list:map( _pipe, fun pig@skill:skill_to_system_fragment/1 ), gleam@string:join(_pipe@1, <<"\n"/utf8>>) end, [Skill_fragment | Fragments] end, Tool_prompts = pig@tool:list_tool_prompts( erlang:element(3, Config_with_librarian) ), Fragments@2 = case Tool_prompts of [] -> Fragments@1; Prompts -> Tool_lines = begin _pipe@2 = Prompts, _pipe@3 = gleam@list:map( _pipe@2, fun(Tp) -> <<<<<<"- "/utf8, (erlang:element(2, Tp))/binary>>/binary, ": "/utf8>>/binary, (erlang:element(3, Tp))/binary>> end ), gleam@string:join(_pipe@3, <<"\n"/utf8>>) end, Tool_fragment = <<"Available tools:\n"/utf8, Tool_lines/binary>>, [Tool_fragment | Fragments@1] end, case Fragments@2 of [] -> Config_with_librarian; _ -> Combined = case erlang:element(4, Config_with_librarian) of {some, Existing} -> <<<>/binary, (gleam@string:join( lists:reverse(Fragments@2), <<"\n\n"/utf8>> ))/binary>>; none -> gleam@string:join( lists:reverse(Fragments@2), <<"\n\n"/utf8>> ) end, pig@agent@state:with_system_prompt(Config_with_librarian, Combined) end. -file("src/pig.gleam", 287). -spec start_with_session(pig_config(), gleam@option:option(loaded_session())) -> {ok, agent()} | {error, start_error()}. start_with_session(Config, Loaded_session) -> Final_config = build_agent_config(Config), case pig@obs@dispatcher:start() of {ok, Dispatcher_subject} -> Consumer_results = gleam@list:map( erlang:element(4, Config), fun(Entry) -> (erlang:element(4, Entry))() end ), Failed = gleam@list:find(Consumer_results, fun(R) -> case R of {error, _} -> true; {ok, _} -> false end end), case Failed of {ok, {error, E}} -> gleam@erlang@process:send(Dispatcher_subject, stop), {error, {actor_start, E}}; _ -> Consumer_subjects = gleam@list:filter_map( Consumer_results, fun(R@1) -> R@1 end ), gleam@list:each( Consumer_subjects, fun(Consumer_subject) -> gleam@erlang@process:send( Dispatcher_subject, {register_consumer, Consumer_subject} ) end ), Runtime_config = {runtime_config, erlang:element(2, Final_config), erlang:element(3, Final_config), erlang:element(5, Config), Dispatcher_subject, erlang:element(6, Final_config), erlang:element(5, Final_config)}, Agent_st = case Loaded_session of {some, {loaded_session, _, Loaded}} -> gleam@list:fold( pig@agent@state:strip_system_messages( erlang:element(3, Loaded) ), pig@agent@state:new(Final_config), fun pig@agent@state:add_message/2 ); none -> case erlang:element(12, Final_config) of {some, Path} -> St = pig@agent@state:new(Final_config), case pig@obs@session:replay(Path) of {ok, Replayed} -> gleam@list:fold( pig@agent@state:strip_system_messages( Replayed ), St, fun pig@agent@state:add_message/2 ); {error, Err} -> logging:log( warning, <<<<<<"Session replay failed for "/utf8, Path/binary>>/binary, ": "/utf8>>/binary, (gleam@string:inspect(Err))/binary>> ), St end; none -> pig@agent@state:new(Final_config) end end, Agent_st@1 = gleam@list:fold( pig@agent@state:strip_system_messages( erlang:element(6, Config) ), Agent_st, fun pig@agent@state:add_message/2 ), Runtime_session = case Loaded_session of {some, {loaded_session, Store, Loaded@1}} -> {session_ready, Store, erlang:element(2, Loaded@1)}; none -> session_disabled end, Rt_state = {runtime_state, Agent_st@1, Runtime_config, Runtime_session}, case pig@agent@runtime:start_with_state( Runtime_config, Rt_state ) of {ok, Subject} -> {ok, {agent, Subject}}; {error, E@1} -> gleam@erlang@process:send(Dispatcher_subject, stop), {error, {actor_start, E@1}} end end; {error, E@2} -> {error, {actor_start, E@2}} end. -file("src/pig.gleam", 262). ?DOC( " Start an agent actor from the config.\n" "\n" " Builds the final `AgentConfig`: registers the librarian tool if\n" " skills are present, composes system prompt from skill descriptions.\n" " Also creates a dispatcher actor and registers all configured consumers.\n" " Returns an `Agent` handle for sending prompts.\n" ). -spec start(pig_config()) -> {ok, agent()} | {error, start_error()}. start(Config) -> case {erlang:element(7, Config), erlang:element(6, Config)} of {{some, _}, [_ | _]} -> {error, {invalid_configuration, <<"SessionStore cannot be combined with non-empty initial_history"/utf8>>}}; {_, _} -> Loaded_session = case erlang:element(7, Config) of none -> {ok, none}; {some, Store} -> {session_store, Load, _} = Store, _pipe = Load(), gleam@result:map( _pipe, fun(Session) -> {some, {loaded_session, Store, Session}} end ) end, case Loaded_session of {error, Error} -> {error, {session_load, Error}}; {ok, Session@1} -> start_with_session(Config, Session@1) end end. -file("src/pig.gleam", 408). ?DOC(" Run a prompt against the agent with an explicit timeout in milliseconds.\n"). -spec run_with_timeout(agent(), binary(), integer()) -> {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}. run_with_timeout(Agent, Prompt, Timeout_ms) -> pig@agent@runtime:run(erlang:element(2, Agent), Prompt, Timeout_ms). -file("src/pig.gleam", 403). ?DOC(" Run a prompt against the agent with a 120-second default timeout.\n"). -spec run(agent(), binary()) -> {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}. run(Agent, Prompt) -> run_with_timeout(Agent, Prompt, 120000). -file("src/pig.gleam", 420). ?DOC( " Run a prompt against the agent with an explicit timeout in milliseconds.\n" "\n" " Returns `Error(Nil)` if the call times out or the agent crashes,\n" " instead of panicking. Use this when you need resilience over panic-on-timeout.\n" ). -spec try_run_with_timeout(agent(), binary(), integer()) -> {ok, {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}} | {error, nil}. try_run_with_timeout(Agent, Prompt, Timeout_ms) -> pig@agent@runtime:try_run(erlang:element(2, Agent), Prompt, Timeout_ms). -file("src/pig.gleam", 436). ?DOC( " Resume the agent loop from its current history.\n" "\n" " Used for the durability pattern: an external system checkpoints messages,\n" " and on retry, rebuilds the agent's history from those checkpoints via\n" " `with_initial_history`. This function continues the loop from where\n" " the history left off, without adding a new user prompt.\n" "\n" " Returns the final assistant message when the loop completes, or an error.\n" ). -spec run_continue_with_timeout(agent(), integer()) -> {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}. run_continue_with_timeout(Agent, Timeout_ms) -> pig@agent@runtime:run_continue(erlang:element(2, Agent), Timeout_ms). -file("src/pig.gleam", 450). ?DOC( " Resume the agent loop from its current history with an explicit timeout.\n" "\n" " Returns `Error(Nil)` if the call times out or the agent crashes, instead of\n" " panicking. The inner result preserves the agent's response or `RunError`.\n" "\n" " A timeout does not cancel in-flight provider or tool work, which may continue\n" " in the background.\n" ). -spec try_run_continue_with_timeout(agent(), integer()) -> {ok, {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}} | {error, nil}. try_run_continue_with_timeout(Agent, Timeout_ms) -> pig@agent@runtime:try_run_continue(erlang:element(2, Agent), Timeout_ms). -file("src/pig.gleam", 458). ?DOC(" Resume the agent loop with a 120-second default timeout.\n"). -spec run_continue(agent()) -> {ok, pig_protocol@message:message()} | {error, pig@run_error:run_error()}. run_continue(Agent) -> run_continue_with_timeout(Agent, 120000). -file("src/pig.gleam", 463). ?DOC(" Stop the agent actor.\n"). -spec stop(agent()) -> nil. stop(Agent) -> pig@agent@runtime:stop(erlang:element(2, Agent)). -file("src/pig.gleam", 468). ?DOC(" Get the agent's current conversation history (all messages).\n"). -spec history(agent()) -> list(pig_protocol@message:message()). history(Agent) -> pig@agent@runtime:history(erlang:element(2, Agent), 5000). -file("src/pig.gleam", 477). ?DOC( " Return a PigConfig with a deterministic mock provider.\n" "\n" " The mock provider always returns\n" " `Assistant(\"mock response\", [], None, None)`.\n" " Useful for testing code that uses pig without hitting a real API.\n" ). -spec test_harness() -> pig_config(). test_harness() -> Response = {assistant, <<"mock response"/utf8>>, [], none, none}, new(fun(_, _) -> {ok, pig@provider:from_message(Response)} end).