mlld Elixir SDK

Elixir wrapper for mlld using a persistent NDJSON JSON-RPC transport over mlld live --stdio.

This SDK intentionally matches the behavior and option model used by the Go, Python, Ruby, and Rust SDKs in sdk/, while adding BEAM-native features for supervision, pooling, and telemetry.

Status

Table Of Contents

  1. Requirements
  2. Installation
  3. Quick Start
  4. API At A Glance
  5. Core Types
  6. Option Reference
  7. Request Lifecycle And Transport Model
  8. Async Handles
  9. State Updates And Cancellation
  10. Supervision And Named Clients
  11. Connection Pool (Mlld.Pool)
  12. Telemetry
  13. Phoenix Channel Bridge
  14. Error Model
  15. Behavioral Parity With Other SDKs
  16. Testing
  17. Operational Notes
  18. Release Process
  19. Future Feature Readiness (VFS / Checkpoint / Resume / Fork)

1. Requirements

2. Installation

From this repo checkout:

cd sdk/elixir
mix deps.get

For local development against this repository's CLI build:

3. Quick Start

alias Mlld.Client

{:ok, client} =
  Client.start_link(
    command: "mlld",
    timeout: 30_000
  )

# String script execution
{:ok, output} =
  Client.process(
    client,
    "/show \"Hello World\"\n",
    mode: :strict
  )

IO.puts(output)

# File execution with payload/state/dynamic modules
{:ok, result} =
  Client.execute(
    client,
    "./agent.mld",
    %{"text" => "hello"},
    state: %{"count" => 0},
    dynamic_modules: %{
      "@config" => %{"mode" => "demo"}
    },
    timeout: 10_000
  )

IO.puts(result.output)
IO.inspect(result.state_writes)

Client.stop(client)

4. API At A Glance

Mlld.Client

Mlld.Handle

Module-level convenience (Mlld)

5. Core Types

Returned structs are aligned with other SDK wrappers:

6. Option Reference

Client options (start_link/1)

Local repo CLI example:

{:ok, client} =
  Mlld.Client.start_link(
    command: "node",
    command_args: ["/absolute/path/to/dist/cli.cjs"],
    timeout: 20_000
  )

Process options (process/3, process_async/3)

Execute options (execute/4, execute_async/4)

Update-state options (update_state/5, Handle.update_state/4)

Behavior:

7. Request Lifecycle And Transport Model

Internals are intentionally consistent with other SDK implementations:

  1. Client keeps one persistent subprocess via mlld live --stdio
  2. Each call is encoded as JSON-RPC line with integer id
  3. Multiple requests are in-flight concurrently (multiplexed)
  4. NDJSON envelopes are decoded from stdout
  5. event messages are routed by id and accumulated for handle result
  6. result message resolves request and unblocks waiters
  7. Transport death fails all pending requests with TRANSPORT_ERROR
  8. Next request lazily restarts transport automatically

8. Async Handles

process_async/3 and execute_async/4 return Mlld.Handle.

{:ok, handle} =
  Mlld.Client.process_async(
    client,
    "loop(99999, 50ms) until @state.exit [\n  continue\n]\nshow \"done\"",
    state: %{"exit" => false},
    timeout: 10_000
  )

# Control in-flight request
:ok = Mlld.Handle.update_state(handle, "exit", true)

# Block until done
{:ok, output} = Mlld.Handle.result(handle)

Task interop patterns:

# Task-returning helpers
task = Mlld.Client.execute_task(client, "pipeline.mld", payload, timeout: 20_000)
{:ok, execute_result} = Task.await(task, :infinity)

# Handle -> underlying task
{:ok, handle} = Mlld.Client.execute_async(client, "pipeline.mld", payload)
task = Mlld.Handle.task(handle)
{:ok, execute_result} = Task.await(task, :infinity)

9. State Updates And Cancellation

Cancel an in-flight request

:ok = Mlld.Handle.cancel(handle)
# or
:ok = Mlld.Client.cancel_request(client, request_id)

Update in-flight state

:ok = Mlld.Handle.update_state(handle, "exit", true)

If request is already complete, update returns:

{:error, %Mlld.Error{code: "REQUEST_NOT_FOUND"}}

10. Supervision And Named Clients

Mlld.Client is a GenServer worker with child spec support.

children = [
  {Mlld.Client,
   name: :main_agent,
   command: "mlld",
   timeout: 60_000}
]

{:ok, _pid} = Supervisor.start_link(children, strategy: :one_for_one)

{:ok, output} = Mlld.Client.process(:main_agent, "/show \"hello\"")

Named process discovery works with standard OTP registration forms.

11. Connection Pool (Mlld.Pool)

Pool provides checkout/checkin and convenience execute/process/analyze helpers.

children = [
  {Mlld.Pool,
   name: :agent_pool,
   size: 20,
   overflow: 5,
   command: "mlld",
   timeout: 30_000}
]

{:ok, _pid} = Supervisor.start_link(children, strategy: :one_for_one)

{:ok, result} = Mlld.Pool.execute(:agent_pool, "pipeline.mld", %{"topic" => "safety"})

Manual checkout:

{:ok, client} = Mlld.Pool.checkout(:agent_pool)
{:ok, output} = Mlld.Client.process(client, "/show \"pooled\"")
:ok = Mlld.Pool.checkin(:agent_pool, client)

Pool notes:

12. Telemetry

The SDK emits :telemetry events with prefix [:mlld, ...].

Core events:

Attach handler example:

:telemetry.attach(
  "mlld-logger",
  [
    [:mlld, :process, :stop],
    [:mlld, :execute, :stop],
    [:mlld, :transport, :restart]
  ],
  fn event, measurements, metadata, _config ->
    IO.inspect({event, measurements, metadata}, label: "mlld telemetry")
  end,
  nil
)

13. Phoenix Channel Bridge

MlldPhoenix.ChannelBridge provides optional event/result forwarding to channel pushes without introducing a hard compile-time dependency on Phoenix.

# inside a Phoenix channel module

def handle_in("execute", %{"filepath" => path, "payload" => payload}, socket) do
  {:ok, _handle} =
    Mlld.Phoenix.stream_execute(
      socket,
      path,
      payload,
      event_topic: "agent:event",
      result_topic: "agent:result"
    )

  {:noreply, socket}
end

If Phoenix is not loaded at runtime:

{:error, :phoenix_not_available}

14. Error Model

All APIs return tuples:

Common error codes:

Pattern matching example:

case Mlld.Client.execute(client, "agent.mld", payload) do
  {:ok, result} ->
    IO.puts(result.output)

  {:error, %Mlld.Error{code: "TIMEOUT"}} ->
    IO.puts("execution timed out")

  {:error, %Mlld.Error{code: code, message: message}} ->
    IO.puts("#{code}: #{message}")
end

15. Behavioral Parity With Other SDKs

Parity guarantees in this implementation:

This makes behavior consistent across Go/Python/Ruby/Rust/Elixir wrappers.

16. Testing

Run in sdk/elixir/:

mix test

Integration tests expect:

Integration coverage mirrors other SDKs:

17. Operational Notes

18. Release Process

Release instructions are maintained in sdk/elixir/RELEASE.md.

Quick path:

cd sdk/elixir
mix format --check-formatted
mix test
mix hex.build
mix hex.publish --dry-run
mix hex.publish

Then tag from repo root:

git tag elixir-sdk-v<version>
git push origin main --tags

19. Future Feature Readiness

The SDK API shape is prepared for upcoming mlld capabilities described in project specs:

As upstream CLI flags/protocol fields land, additions should remain backward-compatible with the current client/handle/result model.