Current section
Files
Jump to
Current section
Files
src/pig.erl
-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} ->
<<<<Existing/binary, "\n\n"/utf8>>/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).