defmodule Jidoka do
@moduledoc """
Public facade for Jidoka.
This module exposes the stable application-facing surface for the Jidoka agent
harness:
* an immutable `Jidoka.Agent.Spec`;
* a compiled `Jidoka.Turn.Plan`;
* a Runic-backed pure planning workflow;
* an `Effect.Intent` / `Effect.Result` interpreter boundary;
* a thin `Jidoka.Harness` execution boundary;
* hibernate/resume from a phase-boundary snapshot.
The facade intentionally uses short, stable verbs for the main workflow:
* `agent/1` builds definition data;
* `plan/1` compiles definition data into executable turn data;
* `turn/3` runs one full model/tool turn;
* `chat/3` runs a turn and returns only final assistant text;
* `chat_async/3`, `stream/2`, and `await/2` support UI-friendly async flows;
* `session/2` starts durable multi-turn state;
* `resume/2` continues from a hibernated snapshot;
* `export/2` writes portable JSON/YAML agent data;
* `inspect/2`, `preflight/3`, and `project/1` expose debugging views.
"""
alias Jidoka.Agent
alias Jidoka.Chat
alias Jidoka.Error
alias Jidoka.Harness
alias Jidoka.Harness.Session
alias Jidoka.Inspection
alias Jidoka.Runtime.AgentServerState
alias Jidoka.Runtime.AgentSnapshot
alias Jidoka.Runtime.Signals
alias Jidoka.Turn
@type agent_input :: Agent.Spec.t() | keyword() | map()
@type plan_input :: Agent.Spec.t() | Turn.Plan.t() | keyword() | map()
@type request_input :: Turn.Request.t() | String.t() | keyword() | map()
@type runtime_opts :: keyword()
@type server_ref :: Jido.AgentServer.server()
@type runnable_input :: plan_input() | server_ref()
@type chat_input :: runnable_input() | Session.t()
@type run_result ::
{:ok, Turn.Result.t()}
| {:hibernate, AgentSnapshot.t()}
| {:error, term()}
defguardp is_server_ref(server)
when is_pid(server) or is_binary(server) or
(is_tuple(server) and tuple_size(server) == 3)
@doc """
Builds a validated agent definition.
Use this when constructing an agent from data at runtime, in tests, or from
tooling that does not use the Spark DSL. The returned `Agent.Spec` is
immutable definition data: it contains the agent id, model, instructions,
controls, operations, context schema, result schema, and memory policy. It is
not a process, session, provider client, or live capability bundle.
"""
@spec agent(keyword() | map()) :: {:ok, Agent.Spec.t()} | {:error, term()}
def agent(attrs), do: Agent.Spec.new(attrs)
@doc """
Builds a validated agent definition and raises when validation fails.
This is useful for compile-time examples, tests, and boot-time application
setup where invalid agent data should fail fast.
"""
@spec agent!(keyword() | map()) :: Agent.Spec.t()
def agent!(attrs), do: Agent.Spec.new!(attrs)
@doc """
Imports a JSON or YAML agent document string into `Jidoka.Agent.Spec`.
Import is intentionally string-only at the facade. File loading, registries,
and trust boundaries belong to the caller; Jidoka owns parsing, normalization,
schema validation, and data-safe conversion into `Agent.Spec`.
"""
@spec import(String.t(), keyword()) :: {:ok, Agent.Spec.t()} | {:error, term()}
def import(contents, opts \\ []), do: Jidoka.Import.import(contents, opts)
@doc """
Exports an agent definition to a portable JSON or YAML document string.
Export writes data that can be passed back into `import/2`. Runtime-only
values are not serialized. If a context or result schema is present, provide
a registry ref with `context_schema_ref:` or `result_schema_ref:` so the
exported document can be resolved by the importing application.
"""
@spec export(module() | Agent.Spec.t() | Turn.Plan.t() | keyword() | map(), keyword()) ::
{:ok, String.t()} | {:error, term()}
def export(agent_or_spec, opts \\ []), do: Jidoka.Export.export(agent_or_spec, opts)
@doc """
Starts a Jidoka DSL agent under the default `Jidoka.Jido` process tree.
The started process is a `Jido.AgentServer`; incoming Jidoka turn signals are
routed to the Runic harness and the result is written back to Jido agent state.
"""
@spec start_agent(module() | Jido.Agent.t(), keyword()) :: DynamicSupervisor.on_start_child()
def start_agent(agent, opts \\ []) when is_atom(agent) or is_struct(agent) do
Jidoka.Jido.start_agent(agent, opts)
end
@doc """
Stops a process-hosted Jidoka agent by pid or registered Jido agent id.
"""
@spec stop_agent(pid() | String.t(), keyword()) :: :ok | {:error, :not_found}
def stop_agent(pid_or_id, opts \\ []), do: Jidoka.Jido.stop_agent(pid_or_id, opts)
@doc """
Looks up a running Jidoka agent process by registered Jido agent id.
This is intentionally a process-hosting helper. It does not build specs,
create sessions, or run turns.
"""
@spec whereis(String.t(), keyword()) :: pid() | nil
def whereis(id, opts \\ []), do: Jidoka.Jido.whereis(id, opts)
@doc """
Starts a durable Jidoka session for an agent, spec, or plan.
A session stores semantic conversation state, the latest turn result,
hibernation snapshots, and replay data. Use it when a caller needs durable
multi-turn behavior instead of a one-off `turn/3`.
"""
@spec session(Jidoka.Session.agent_input()) :: {:ok, Jidoka.Session.t()} | {:error, term()}
@spec session(Jidoka.Session.agent_input(), keyword() | String.t()) ::
{:ok, Jidoka.Session.t()} | {:error, term()}
def session(agent_or_plan, opts \\ []), do: Jidoka.Session.start(agent_or_plan, opts)
@doc """
Starts a durable Jidoka session with an explicit session id.
Prefer this arity when the caller already has an application-level
conversation id and wants Jidoka session state to be addressable by that id.
"""
@spec session(Jidoka.Session.agent_input(), String.t(), keyword()) ::
{:ok, Jidoka.Session.t()} | {:error, term()}
def session(agent_or_plan, session_id, opts) when is_binary(session_id) and is_list(opts) do
Jidoka.Session.start(agent_or_plan, session_id, opts)
end
@doc """
Returns the current handoff owner for a conversation, if one has been recorded.
Handoffs are durable routing data. They indicate which agent should own
future turns for a conversation after a handoff operation succeeds.
"""
@spec handoff(String.t()) :: Jidoka.Handoff.OwnerStore.owner() | nil
def handoff(conversation_id), do: Jidoka.Handoff.OwnerStore.owner(conversation_id)
@doc """
Clears the current handoff owner for a conversation.
Use this when an application wants to return routing control to its default
agent selection logic.
"""
@spec reset_handoff(String.t()) :: :ok
def reset_handoff(conversation_id), do: Jidoka.Handoff.OwnerStore.reset(conversation_id)
@doc """
Compiles an agent definition into executable turn data.
`Turn.Plan` is still pure data. It contains no live capabilities, processes,
provider clients, or credentials. Use it when you want to inspect or cache the
normalized runtime contract before executing a turn.
"""
@spec plan(plan_input()) :: {:ok, Turn.Plan.t()} | {:error, term()}
def plan(%Turn.Plan{} = plan), do: {:ok, plan}
def plan(spec_input) do
with {:ok, spec} <- Agent.Spec.from_input(spec_input) do
Turn.Plan.new(spec)
end
end
@doc """
Compiles an agent definition into executable turn data and raises on failure.
This mirrors `plan/1`, but is intended for setup paths where invalid agent
data should stop execution immediately.
"""
@spec plan!(plan_input()) :: Turn.Plan.t()
def plan!(%Turn.Plan{} = plan), do: plan
def plan!(spec_input), do: spec_input |> Agent.Spec.from_input() |> plan_from_agent!()
@doc """
Runs one turn and returns final assistant text.
`chat/3` is the ergonomic path for product code that only needs the final
assistant answer. For caller-managed sessions, the updated session is returned
alongside the text so durable state is not lost.
Use `turn/3` when callers need the full `Turn.Result`, event journal, agent
state, operation results, stream events, or hibernation snapshot.
"""
@spec chat(chat_input(), String.t(), runtime_opts()) ::
{:ok, String.t()}
| {:ok, Jidoka.Session.t(), String.t()}
| {:hibernate, AgentSnapshot.t()}
| {:hibernate, Jidoka.Session.t(), AgentSnapshot.t()}
| {:error, term()}
def chat(spec_or_server, input, opts \\ [])
def chat(%Session{} = session, input, opts) when is_binary(input) and is_list(opts) do
case Jidoka.Session.chat(session, input, opts) do
{:ok, session, content} -> {:ok, session, content}
{:hibernate, session, snapshot} -> {:hibernate, session, snapshot}
{:error, reason} -> {:error, Error.normalize(reason, operation: :chat, phase: :session)}
end
end
def chat(server, input, opts)
when is_binary(input) and is_server_ref(server) and is_list(opts) do
with {:ok, %Turn.Result{content: content}} <- turn(server, input, opts) do
{:ok, content}
end
end
def chat(spec_input, input, opts) when is_binary(input) do
with {:ok, %Turn.Result{content: content}} <- turn(spec_input, input, opts) do
{:ok, content}
end
end
@doc """
Starts one chat request asynchronously and returns a request handle.
This is the UI-friendly companion to `chat/3`. Pass `stream: true` to stream
request-scoped `Jidoka.Event` values to the caller mailbox while the task is
running. Use `stream/2` to enumerate those events and `await/2` to collect the
final normalized chat result.
"""
@spec chat_async(chat_input(), String.t(), runtime_opts()) ::
{:ok, Chat.Request.t()} | {:error, term()}
def chat_async(target, input, opts \\ []) when is_binary(input) and is_list(opts) do
Chat.Request.start(target, input, opts)
end
@doc """
Builds a request-scoped event stream for an async chat request.
The stream consumes events already emitted to the caller mailbox and stops at
`:turn_finished`, `:turn_failed`, or `:turn_hibernated`.
"""
@spec stream(Chat.Request.t(), keyword()) :: Jidoka.Stream.t()
def stream(%Chat.Request{} = request, opts \\ []), do: Jidoka.Stream.new(request, opts)
@doc """
Waits for a chat request or stream to finish.
This returns the same normalized result shape as `chat/3`, including session
results when the request target is a `Jidoka.Session`.
"""
@spec await(Chat.Request.t() | Jidoka.Stream.t(), keyword()) :: term()
def await(request_or_stream, opts \\ [])
def await(%Chat.Request{} = request, opts), do: Chat.Request.await(request, opts)
def await(%Jidoka.Stream{} = stream, opts), do: Jidoka.Stream.await(stream, opts)
@doc """
Runs one agent turn through the Jidoka Runic spine.
This is the stable core runtime entrypoint. It accepts an `Agent.Spec` or
`Turn.Plan`, normalizes the request, runs pure workflow planning, interprets
external effects through explicit runtime capabilities, and returns a typed
result or snapshot.
Use `turn/3` for deterministic tests with injected capabilities, live ReqLLM
calls, process-hosted agents, controls, tools, hibernation, streaming, and
trace/event inspection.
"""
@spec turn(runnable_input(), request_input(), runtime_opts()) :: run_result()
def turn(spec_or_server, request_input, opts \\ [])
def turn(server, input, opts)
when is_binary(input) and is_server_ref(server) and is_list(opts) do
timeout = Keyword.get(opts, :timeout, 30_000)
runtime_opts =
opts
|> Keyword.drop([:context, :metadata, :request_id, :timeout])
|> Keyword.merge(Keyword.get(opts, :runtime_opts, []))
signal =
Signals.turn_run(input,
request_id: Keyword.get(opts, :request_id),
context: Keyword.get(opts, :context),
metadata: Keyword.get(opts, :metadata),
runtime_opts: runtime_opts
)
result =
with {:ok, server} <- resolve_server_ref(server),
{:ok, agent} <- Jido.AgentServer.call(server, signal, timeout) do
run_result_from_jido_agent(agent)
end
case result do
{:ok, _result} = ok ->
ok
{:hibernate, _snapshot} = hibernate ->
hibernate
{:error, reason} ->
{:error,
Error.normalize(reason,
operation: :turn,
phase: :agent_server,
target: server,
request_id: Keyword.get(opts, :request_id)
)}
end
end
def turn(spec_or_plan, request_input, opts) do
case Harness.run_turn(spec_or_plan, request_input, opts) do
{:ok, _result} = ok ->
ok
{:hibernate, _snapshot} = hibernate ->
hibernate
{:error, reason} ->
{:error, Error.normalize(reason, operation: :turn, phase: :harness)}
end
end
@doc """
Awaits terminal Jido status for a process-hosted Jidoka agent.
This helper is only for process-hosted agents started through Jido. It is not
needed for direct `turn/3` or `chat/3` calls.
"""
@spec await_agent(server_ref(), keyword()) :: {:ok, map()} | {:error, term()}
def await_agent(server, opts \\ []) do
result =
with {:ok, server} <- resolve_server_ref(server) do
Jido.AgentServer.await_completion(server, opts)
end
case result do
{:ok, _result} = ok ->
ok
{:error, reason} ->
{:error, Error.normalize(reason, operation: :await_agent, target: server)}
end
end
@doc """
Resumes from a durable agent snapshot.
The snapshot may be an `AgentSnapshot` struct, map-shaped snapshot data, or
the opaque string returned by `Jidoka.Runtime.AgentSnapshot.serialize/1`.
Resume continues through the same harness boundary as `turn/3`, so callers
provide the same runtime capabilities plus any required approval response.
"""
@spec resume(AgentSnapshot.t() | keyword() | map() | String.t(), runtime_opts()) :: run_result()
def resume(snapshot_input, opts \\ []) do
case Harness.resume(snapshot_input, opts) do
{:ok, _result} = ok ->
ok
{:hibernate, _snapshot} = hibernate ->
hibernate
{:error, reason} ->
{:error, Error.normalize(reason, operation: :resume, phase: :harness)}
end
end
@doc """
Formats a Jidoka error or arbitrary error term for display.
This is intended for UI/logging boundaries that need a concise message rather
than a full Splode error struct.
"""
@spec format_error(term()) :: String.t()
def format_error(error), do: Error.format(error)
@doc """
Converts a Jidoka error or arbitrary error term into a display-oriented map.
Values likely to contain credentials are sanitized before being returned.
"""
@spec error_to_map(term()) :: map()
def error_to_map(error), do: Error.to_map(error)
@doc """
Returns a stable inspection view for an agent, plan, turn, snapshot, journal,
or other Jidoka data value.
`inspect/2` is the human-facing debug surface. It favors grouped, readable
maps over raw structs.
"""
@spec inspect(term(), keyword()) :: term()
def inspect(value, opts \\ []), do: Inspection.inspect(value, opts)
@doc """
Assembles the prompt for a turn without calling an LLM or tools.
Use preflight to debug prompt assembly, tool metadata, memory injection, and
request normalization before running live effects.
"""
@spec preflight(plan_input() | module(), request_input(), runtime_opts()) ::
{:ok, Inspection.Preflight.t()} | {:error, term()}
def preflight(spec_or_plan, request_input, opts \\ []) do
case Inspection.preflight(spec_or_plan, request_input, opts) do
{:ok, _preflight} = ok ->
ok
{:error, reason} ->
{:error, Error.normalize(reason, operation: :preflight)}
end
end
@doc """
Projects a Jidoka data contract into a stable inspection map.
`project/1` is the data-facing companion to `inspect/2`. It returns compact,
deterministic maps that are useful for tests, golden files, traces, and UI
rendering.
"""
@spec project(term()) :: term()
def project(value), do: Jidoka.Projection.project(value)
@doc """
Normalizes any error term into a Splode-backed `Jidoka.Error` exception.
Prefer returning normalized errors from facade boundaries so callers see a
consistent error shape even when the underlying cause came from a provider,
store, control, or runtime capability.
"""
@spec normalize_error(term(), keyword() | map()) :: Exception.t()
def normalize_error(reason, context \\ %{}), do: Error.normalize(reason, context)
defp plan_from_agent!({:ok, %Agent.Spec{} = spec}), do: Turn.Plan.new!(spec)
defp plan_from_agent!({:error, reason}),
do: raise(ArgumentError, "invalid agent spec: #{Kernel.inspect(reason)}")
defp run_result_from_jido_agent(%Jido.Agent{state: state}) do
case AgentServerState.from_jido_state(state) do
{:ok, agent_server_state} ->
AgentServerState.to_run_result(agent_server_state)
{:error, reason} ->
{:error, Error.normalize(reason, operation: :turn, phase: :agent_server)}
end
end
defp resolve_server_ref(server) when is_binary(server) do
case whereis(server) do
nil -> {:error, :not_found}
pid -> {:ok, pid}
end
end
defp resolve_server_ref(server), do: {:ok, server}
end