Current section

Files

Jump to
fountain_sdk README.md
Raw

README.md

# Fountain Elixir SDK
The official Elixir client for [Fountain](https://github.com/BinaryBourbon/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`:
```elixir
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:
1. Explicit `:api_key` and `:base_url` options
2. `FOUNTAIN_API_KEY` (or sandbox-scoped `FOUNTAIN_TOKEN`) and `FOUNTAIN_BASE_URL`
3. The selected profile in `~/.fountain/credentials`
4. The hosted Fountain endpoint
```elixir
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.
```elixir
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:
```elixir
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
```elixir
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
```elixir
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:
```elixir
{: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
```elixir
{: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`.
```elixir
{: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`.
```elixir
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](LICENSE).