Jido.Harness is a supervised Elixir runtime for coding-agent CLIs. It turns Amp, Claude Code, Codex, Gemini CLI, Grok, Kimi Code, OpenCode, Pi, and Z.AI into caller-independent BEAM resources with one normalized API.
Provider-specific protocols are translated into validated requests, terminal results, ordered events, readiness information, capabilities, and errors. Applications consume ordinary Jido.Harness structs instead of parsing each CLI's JSON or depending on provider SDKs.
- Blocking one-shot requests through
Jido.Harness.run/3. - Detached supervised runs that can be listed, streamed, replayed, awaited, cancelled, and pruned by stable ID.
- Multi-turn sessions with queued follow-ups and transport-aware interaction capabilities.
- Structured local process management using executable plus argv, with stdin, PTY, timeouts, process-group cancellation, and retained output.
- Pull-based cursor streams and bounded replay journals for slow or reconnecting consumers.
- Provider readiness checks, explicit installation recipes, telemetry, and reusable live integration contracts.
Runs, sessions, and managed processes belong to the application supervision tree rather than the process that starts or consumes them. They survive caller and stream-consumer exits, but intentionally do not survive a BEAM or host restart.
| Provider | Atom | CLI | Default session transport |
|---|---|---|---|
| Amp | :amp |
amp |
resumed stream JSON |
| Claude Code | :claude |
claude |
resumed stream JSON |
| Codex | :codex |
codex |
resumed exec JSONL |
| Gemini CLI | :gemini |
gemini |
resumed stream JSON |
| Grok | :grok |
grok |
resumed streaming JSON |
| Kimi Code | :kimi |
kimi |
persistent ACP |
| OpenCode | :opencode |
opencode |
persistent ACP |
| Pi | :pi |
pi |
persistent JSONL RPC |
| Z.AI | :zai |
claude |
resumed stream JSON |
Provider capabilities and normalized options differ. Jido.Harness advertises those differences and rejects unsupported options instead of silently ignoring them. See the provider guide.
def deps do
[
{:jido_harness, "~> 2.0"}
]
endThe built-in adapters are registered automatically. Configure a default only when you want providerless calls:
config :jido_harness,
default_provider: :codex,
provider_config: %{
codex: %{
request_defaults: %{sandbox_mode: :workspace_write}
}
}Check local CLIs without sending a prompt:
mix jido_harness.check
mix jido_harness.check --providers codex,kimi --strictFor one optional live smoke request through exactly one provider:
mix jido_harness.chat codexThe chat task may consume paid API or subscription usage.
{:ok, %Jido.Harness.RunResult{} = result} =
Jido.Harness.run(:codex, "Reply with exactly: harness-ready",
cwd: File.cwd!(),
await_timeout: 300_000
)
result.status
#=> :completed
result.text
#=> "harness-ready"The result has the same top-level shape regardless of provider:
%Jido.Harness.RunResult{
run_id: "run_...",
provider: :codex,
provider_session_id: "...",
status: :completed,
text: "harness-ready",
text_truncated?: false,
usage: %{},
events: [%Jido.Harness.Event{}, ...],
metadata: %{},
error: nil
}Normalization does not erase meaningful differences. Shared semantics have
stable fields and event names; optional data is described by provider
capabilities; records without a safe canonical mapping use :provider_event.
See Normalization and the data model.
| Need | API | Result |
|---|---|---|
| Wait for one request | Jido.Harness.run/3 |
RunResult |
| Start work and reattach later | Jido.Harness.Run |
stable run_id |
| Hold a multi-turn conversation | Jido.Harness.Session |
stable session_id and turn_id values |
| Supervise a local executable | Jido.Harness.Process |
stable process_id |
Detached run example:
{:ok, run_id} =
Jido.Harness.Run.start(:codex, %{
prompt: "Review the current branch",
cwd: File.cwd!(),
sandbox_mode: :read_only
})
{:ok, events} = Jido.Harness.Run.stream(run_id)
Enum.each(events, &IO.inspect/1)
{:ok, %Jido.Harness.RunResult{} = result} =
Jido.Harness.Run.await(run_id, 600_000)Interactive session example:
{:ok, session_id} =
Jido.Harness.Session.start(:codex, %{cwd: File.cwd!()})
{:ok, first_id} =
Jido.Harness.Session.send_message(session_id, "Summarize this project")
{:ok, %Jido.Harness.TurnResult{} = first} =
Jido.Harness.Session.await(session_id, first_id, 600_000)
{:ok, next_id} =
Jido.Harness.Session.follow_up(session_id, "Now identify the main risk")
{:ok, %Jido.Harness.TurnResult{} = next} =
Jido.Harness.Session.await(session_id, next_id, 600_000)
:ok = Jido.Harness.Session.close(session_id)Start with:
- Overview
- Getting started
- Choosing a workflow
- Providers and capabilities
- Normalization and the data model
Then follow the workflow guides for one-shot requests, detached runs, interactive sessions, or managed processes.
The repository includes runnable notebooks:
Provider examples are live and may consume usage. The managed-process example is entirely local.
Jido.Harness ships opt-in ExUnit contracts without starting ExUnit itself:
defmodule MyCodexIntegrationTest do
use Jido.Harness.IntegrationCase, provider: :codex
harness_contract_tests()
endSee the testing guide for deterministic, smoke, contract, lifecycle, interactive, and soak profiles.
Jido.Harness is a normalization and lifecycle runtime. It is not a durable job
system, provider router, workspace provisioner, TUI automation layer, or retry
engine. It does not depend on jido, jido_shell, Sprites, Splode, provider
SDKs, or generic subprocess wrappers.
See the dependency policy for the runtime boundary.