Fountain Elixir SDK
The official Elixir client for Fountain. It covers agent runs and follow-ups, reconnecting event streams, permission requests, resources and secrets, teammates and schedules, connections, conversation history, and sandbox lifecycle/files.
Installation
Add fountain_sdk to mix.exs:
def deps do
[{:fountain_sdk, "~> 0.1.0"}]
end
Elixir 1.15 or newer is required.
Authentication
The client resolves credentials the same way as the Fountain CLI:
- Explicit
:api_keyand:base_urloptions FOUNTAIN_API_KEY(or sandbox-scopedFOUNTAIN_TOKEN) andFOUNTAIN_BASE_URL- The selected profile in
~/.fountain/credentials - The hosted Fountain endpoint
client = Fountain.new(api_key: System.fetch_env!("FOUNTAIN_API_KEY"))
When code runs inside a Fountain sandbox, FOUNTAIN_CONVERSATION_ID is automatically sent as parent attribution. HTTPS uses peer and hostname verification with the Erlang/OTP CA store.
Run an agent
Fountain.run/3 starts work immediately. The handle supports one completion wait and any number of event consumers without making a second API request.
client = Fountain.new()
run =
Fountain.run(client, "Upgrade us to Phoenix 1.8 and open a PR",
agent: "reposage",
vault: "github-bot"
)
{:ok, result} = Fountain.Run.await(run)
IO.puts(result.text)
IO.inspect(result.tools_used)
Set timeout: milliseconds on Fountain.run/3, Fountain.Conversation.send/3, or Fountain.Team.message/4 to stop the SDK waiting. A timeout does not interrupt the remote turn; the returned %Fountain.Error{kind: :timeout} contains conversation_id and partial_text so it can be resumed.
Stream all derived and raw events, or only answer text:
for event <- Fountain.Run.stream(run), do: IO.inspect(event)
for text <- Fountain.Run.text_stream(run), do: IO.write(text)
Run events use idiomatic atom keys and :conversation, :turn_start, :text, :thinking, :tool, :permission, :block, :event, and :turn_end types. Raw API payloads remain string-keyed maps.
Permissions and control
for %{type: :permission, request: request} <- Fountain.Run.stream(run) do
allow = Enum.find(request.options, &(&1["kind"] == "allow_once"))
if allow, do: Fountain.Run.answer(run, request.request_id, allow.option_id)
end
Fountain.Run.interrupt(run) # stop the turn; keep the sandbox
Fountain.Run.terminate(run) # tear the sandbox down
Fountain.Run.cancel(run) # stop only this SDK wait
Follow up and inspect conversations
conversation = Fountain.resume(client, result.conversation_id)
follow_up = Fountain.Conversation.send(conversation, "Now add regression tests")
{:ok, next_result} = Fountain.Run.await(follow_up)
{:ok, history} = Fountain.Conversation.history(conversation, streams: [:acp])
{:ok, turns} = Fountain.Conversation.turns(conversation)
:ok = Fountain.Conversation.mark_read(conversation)
history/2 drains every page and requests server-parsed blocks. events/2 returns the raw reconnecting SSE enumerable. Follow-ups discover the end of a cold conversation's stage feed before posting, preventing old output from being replayed as the new answer.
Conversation handles also provide get/1, status/1, tree/1, event_page/3, answer/3, interrupt/1, terminate/1, and delete/1.
Resources and secrets
The client exposes resource handles directly:
{:ok, agents} = Fountain.Agents.list(client.agents)
{:ok, agent} = Fountain.Agents.get(client.agents, "reposage")
{:ok, created} = Fountain.Environments.create(client.environments, environment_definition)
:ok = Fountain.Secrets.delete(client.environments.secrets, "production", "OLD_TOKEN")
{:ok, _} = Fountain.Secrets.set(client.vaults.secrets, "github-bot", "GITHUB_TOKEN", token)
Fountain.Agents, Fountain.Environments, and Fountain.Vaults implement list, get, create, update, and delete. Names resolve case-insensitively, then by unique prefix; UUIDs skip a listing. Resource payloads use the API's string keys so definitions can be shared with REST and fountain.yml.
Teammates and schedules
{:ok, teammates} = Fountain.Team.list(client.team)
run = Fountain.Team.message(client.team, "reviewer", "Review the latest PR")
{:ok, schedule} =
Fountain.TeamSchedules.create(client.team.schedules, "reviewer", %{
"cron" => "0 9 * * 1-5",
"prompt" => "Review open pull requests"
})
Fountain.Team includes list, get, add, remove, rename, message, conversation, history, fresh_conversation, comms_status, and stream. Fountain.TeamSchedules includes list, get, create, update, delete, and run.
Connections and sandboxes
Provider connections are available at client.connections; provider definitions are at client.connections.providers.
{:ok, connections} = Fountain.Connections.list(client.connections)
{:ok, providers} = Fountain.ConnectionProviders.list(client.connections.providers)
{:ok, sandboxes} = Fountain.sandboxes(client, status: ["ready", "suspended"])
{:ok, listing} = Fountain.sandbox_files(client, sandbox_id, "src")
{:ok, file} = Fountain.sandbox_file(client, sandbox_id, "mix.exs", max_bytes: 64_000)
{:ok, diff} = Fountain.sandbox_diff(client, sandbox_id, staged: true)
:ok = Fountain.reset_sandbox(client, sandbox_id)
Connections support list, get, and delete. Providers support list, get, create, update, delete, and MCP discover.
Errors and raw requests
All non-bang public calls return {:ok, value}, :ok, or {:error, %Fountain.Error{}}. Errors expose kind, HTTP status, server code, parsed body, and retry_after. Helpers include Fountain.Error.retryable?/1 and field_errors/1.
case Fountain.request(client, "GET", "/api/new-endpoint") do
{:ok, body} -> body
{:error, %Fountain.Error{kind: :rate_limit, retry_after: seconds}} -> {:retry, seconds}
end
Absolute URLs are accepted only when they have the configured Fountain origin, preventing bearer credentials from being sent cross-origin.
Process ownership
A client resolver cache, conversation cursor, and run server are lightweight process-owned state. Create and use handles from a long-lived process (such as a GenServer) when sharing them. A run and its active HTTP/SSE work are cleaned up when the process that created it exits.
License
Apache-2.0. See LICENSE.