defmodule Jido do use Supervisor alias Jido.Agent.WorkerPool alias Jido.Config.Defaults alias Jido.RuntimeStore @moduledoc """ 自動 (Jido) - An autonomous agent framework for Elixir, built for workflows and multi-agent systems. ## Quick Start Create a Jido supervisor in your application: defmodule MyApp.Jido do use Jido, otp_app: :my_app end Add to your supervision tree: children = [MyApp.Jido] Start and manage agents: {:ok, pid} = MyApp.Jido.start_agent(MyAgent, id: "agent-1") pid = MyApp.Jido.whereis("agent-1") agents = MyApp.Jido.list_agents() :ok = MyApp.Jido.stop_agent("agent-1") ## Core Concepts Jido agents are immutable data structures. The core operation is `cmd/2`: {agent, directives} = MyAgent.cmd(agent, MyAction) - **Agents** — Immutable structs updated via commands - **Actions** — Functions that transform agent state and may perform work - **Directives** — Runtime-owned external effects (signals, processes, etc.) Jido keeps agent decision logic pure. Actions may be pure or effectful. Directives are for effects you want the runtime to own. If a step needs a result back now to continue reasoning or update state, an effectful action is acceptable; if delivery should belong to the runtime or an integration layer, return a directive. ## For Tests Start a unique Jido instance in runtime tests: defmodule MyAgentTest do use ExUnit.Case, async: true setup do jido = :"jido_test_#{System.unique_integer([:positive])}" {:ok, jido_pid} = start_supervised({Jido, name: jido}) {:ok, jido: jido, jido_pid: jido_pid} end test "agent works", %{jido: jido} do {:ok, pid} = Jido.start_agent(jido, MyAgent) # ... end end See `Jido.Agent` for defining agents and `Jido.Await` for coordination. """ @doc """ Creates a Jido supervisor module. ## Options - `:otp_app` - Required. Your application name (e.g., `:my_app`). ## Example defmodule MyApp.Jido do use Jido, otp_app: :my_app end Then add to your supervision tree in `lib/my_app/application.ex`: children = [MyApp.Jido] Optionally configure in `config/config.exs` to customize defaults: config :my_app, MyApp.Jido, max_tasks: 2000, agent_pools: [] """ defmacro __using__(opts) do otp_app = Keyword.fetch!(opts, :otp_app) storage = Keyword.get(opts, :storage, {Jido.Storage.ETS, [table: :jido_storage]}) default_plugins = Keyword.get(opts, :default_plugins, nil) quote location: :keep do @otp_app unquote(otp_app) @doc false @spec __otp_app__() :: unquote(otp_app) def __otp_app__, do: @otp_app @doc "Returns the storage configuration for this Jido instance." @spec __jido_storage__() :: {module(), keyword()} def __jido_storage__, do: Jido.Storage.normalize_storage(unquote(storage)) require Jido.Agent.DefaultPlugins @default_plugins Jido.Agent.DefaultPlugins.resolve_instance_defaults( @otp_app, __MODULE__, unquote(Macro.escape(default_plugins)) ) # The typespec for __default_plugins__ triggers a `contract_supertype` # warning from dialyzer. @dialyzer {:nowarn_function, [__default_plugins__: 0]} @doc "Returns the default plugins for agents bound to this Jido instance." @spec __default_plugins__() :: [module() | {module(), map()}] def __default_plugins__, do: @default_plugins @doc false def child_spec(init_arg \\ []) do opts = config(init_arg) |> Keyword.put_new(:name, __MODULE__) |> Keyword.put_new(:otp_app, @otp_app) Jido.child_spec(opts) end @doc false def start_link(init_arg \\ []) do opts = config(init_arg) |> Keyword.put_new(:name, __MODULE__) |> Keyword.put_new(:otp_app, @otp_app) Jido.start_link(opts) end @doc """ Returns the runtime config for this Jido instance. Configuration is loaded from `config :#{@otp_app}, #{inspect(__MODULE__)}` and overridden by any runtime options passed in. """ @spec config(keyword()) :: keyword() def config(overrides \\ []) do @otp_app |> Application.get_env(__MODULE__, []) |> Keyword.merge(overrides) end defoverridable config: 1 @doc "Starts an agent under this Jido instance." @spec start_agent(module() | struct(), keyword()) :: DynamicSupervisor.on_start_child() def start_agent(agent, opts \\ []) do Jido.start_agent(__MODULE__, agent, opts) end @doc "Stops an agent (by pid or id) under this Jido instance." @spec stop_agent(pid() | String.t(), keyword()) :: :ok | {:error, :not_found} def stop_agent(pid_or_id, opts \\ []) when is_list(opts) do Jido.stop_agent(__MODULE__, pid_or_id, opts) end @doc "Looks up an agent by ID under this Jido instance." @spec whereis(String.t(), keyword()) :: pid() | nil def whereis(id, opts \\ []) when is_binary(id) and is_list(opts) do Jido.whereis(__MODULE__, id, opts) end @doc "Lists all agents under this Jido instance." @spec list_agents(keyword()) :: [{String.t(), pid()}] def list_agents(opts \\ []) when is_list(opts) do Jido.list_agents(__MODULE__, opts) end @doc "Returns the count of running agents under this Jido instance." @spec agent_count(keyword()) :: non_neg_integer() def agent_count(opts \\ []) when is_list(opts) do Jido.agent_count(__MODULE__, opts) end @doc "Returns the Registry name for this Jido instance." @spec registry_name() :: atom() def registry_name, do: Jido.registry_name(__MODULE__) @doc "Returns the AgentSupervisor name for this Jido instance." @spec agent_supervisor_name() :: atom() def agent_supervisor_name, do: Jido.agent_supervisor_name(__MODULE__) @doc "Returns the TaskSupervisor name for this Jido instance." @spec task_supervisor_name() :: atom() def task_supervisor_name, do: Jido.task_supervisor_name(__MODULE__) @doc "Returns the RuntimeStore name for this Jido instance." @spec runtime_store_name() :: atom() def runtime_store_name, do: Jido.runtime_store_name(__MODULE__) @doc "Hibernate an agent to storage." @spec hibernate(Jido.Agent.t(), keyword()) :: :ok | {:error, term()} def hibernate(agent, opts \\ []) when is_list(opts) do Jido.hibernate(__MODULE__, agent, opts) end @doc "Thaw an agent from storage." @spec thaw(module(), term(), keyword()) :: {:ok, Jido.Agent.t()} | {:error, term()} def thaw(agent_module, key, opts \\ []) when is_list(opts) do Jido.thaw(__MODULE__, agent_module, key, opts) end @doc """ Controls debug mode for this Jido instance. - `debug()` — returns current debug level - `debug(:on)` — enable developer-friendly verbosity - `debug(:verbose)` — enable maximum detail - `debug(:off)` — disable debug overrides - `debug(pid)` — toggle per-agent debug mode - `debug(:on, redact: false)` — also disable redaction """ @spec debug() :: Jido.Debug.level() def debug, do: Jido.Debug.level(__MODULE__) @spec debug(Jido.Debug.level() | pid()) :: :ok | {:error, term()} | Jido.Debug.level() def debug(pid) when is_pid(pid), do: Jido.AgentServer.set_debug(pid, true) def debug(level) when is_atom(level), do: Jido.Debug.enable(__MODULE__, level) @spec debug(Jido.Debug.level(), keyword()) :: :ok def debug(level, opts) when is_atom(level), do: Jido.Debug.enable(__MODULE__, level, opts) @doc "Returns recent debug events from an agent's ring buffer." @spec recent(pid(), non_neg_integer()) :: {:ok, [map()]} | {:error, term()} def recent(pid, limit \\ 50), do: Jido.AgentServer.recent_events(pid, limit: limit) @doc "Returns the current debug status for this instance." @spec debug_status() :: map() def debug_status, do: Jido.Debug.status(__MODULE__) end end @type agent_id :: String.t() | atom() @type partition :: term() # Default instance name for scripts/Livebook @default_instance Jido.Default @doc """ Returns the default Jido instance name. Used by `Jido.start/1` for scripts and Livebook quick-start. """ @spec default_instance() :: atom() def default_instance, do: @default_instance # --------------------------------------------------------------------------- # Debug API (default instance delegates) # --------------------------------------------------------------------------- @doc """ Controls debug mode for the default Jido instance (`Jido.Default`). - `debug()` — returns current debug level - `debug(:on)` — enable developer-friendly verbosity - `debug(:verbose)` — enable maximum detail - `debug(:off)` — disable debug overrides """ @spec debug() :: Jido.Debug.level() def debug, do: Jido.Debug.level(@default_instance) @spec debug(Jido.Debug.level()) :: :ok def debug(level) when is_atom(level), do: Jido.Debug.enable(@default_instance, level) @spec debug(Jido.Debug.level(), keyword()) :: :ok def debug(level, opts) when is_atom(level), do: Jido.Debug.enable(@default_instance, level, opts) @doc """ Start the default Jido instance for scripts and Livebook. This is an idempotent convenience function - safe to call multiple times (returns `{:ok, pid}` even if already started). ## Examples # In a script or Livebook {:ok, _} = Jido.start() {:ok, pid} = Jido.start_agent(Jido.default_instance(), MyAgent) # With custom options {:ok, _} = Jido.start(max_tasks: 2000) ## Options Same as `start_link/1`, but `:name` defaults to `Jido.Default`. """ @spec start(keyword()) :: {:ok, pid()} | {:error, term()} def start(opts \\ []) do opts = Keyword.put_new(opts, :name, @default_instance) case start_link(opts) do {:ok, pid} -> {:ok, pid} {:error, {:already_started, pid}} -> {:ok, pid} other -> other end end @doc """ Stop a Jido instance. Defaults to stopping the default instance (`Jido.Default`). ## Examples Jido.stop() Jido.stop(MyApp.Jido) """ @spec stop(atom()) :: :ok def stop(name \\ @default_instance) do case Process.whereis(name) do nil -> :ok pid -> Supervisor.stop(pid) end end @doc """ Starts a Jido instance supervisor. ## Options - `:name` - Required. The name of this Jido instance (e.g., `MyApp.Jido`) ## Example {:ok, pid} = Jido.start_link(name: MyApp.Jido) """ def start_link(opts) do name = Keyword.fetch!(opts, :name) Supervisor.start_link(__MODULE__, opts, name: name) end @doc false def child_spec(opts) do name = Keyword.fetch!(opts, :name) %{ id: name, start: {__MODULE__, :start_link, [opts]}, type: :supervisor, restart: :permanent, shutdown: Defaults.jido_shutdown_timeout_ms() } end @impl true def init(opts) do name = Keyword.fetch!(opts, :name) runtime_store = runtime_store_name(name) if otp_app = opts[:otp_app] do Jido.Debug.maybe_enable_from_config(otp_app, name) end :ok = Jido.RuntimeStore.ensure_table(runtime_store) base_children = [ {Task.Supervisor, name: task_supervisor_name(name), max_children: Keyword.get(opts, :max_tasks, 1000)}, {Registry, keys: :unique, name: registry_name(name)}, {Jido.RuntimeStore, name: runtime_store}, {DynamicSupervisor, name: agent_supervisor_name(name), strategy: :one_for_one, max_restarts: 1000, max_seconds: 5} ] pool_children = WorkerPool.build_pool_child_specs(name, Keyword.get(opts, :agent_pools, [])) Supervisor.init(base_children ++ pool_children, strategy: :one_for_one) end @doc """ Generate a unique identifier. Delegates to `Jido.Util.generate_id/0`. """ defdelegate generate_id(), to: Jido.Util @doc "Returns the Registry name for a Jido instance." @spec registry_name(atom()) :: atom() def registry_name(name), do: Module.concat(name, Registry) @doc "Returns the AgentSupervisor name for a Jido instance." @spec agent_supervisor_name(atom()) :: atom() def agent_supervisor_name(name), do: Module.concat(name, AgentSupervisor) @doc "Returns the TaskSupervisor name for a Jido instance." @spec task_supervisor_name(atom()) :: atom() def task_supervisor_name(name), do: Module.concat(name, TaskSupervisor) @doc "Returns the RuntimeStore name for a Jido instance." @spec runtime_store_name(atom()) :: atom() def runtime_store_name(name), do: Module.concat(name, RuntimeStore) @doc "Returns the Scheduler name for a Jido instance." @spec scheduler_name(atom()) :: atom() def scheduler_name(name), do: Module.concat(name, Scheduler) @doc "Returns the AgentPool name for a specific pool in a Jido instance." @spec agent_pool_name(atom(), atom()) :: atom() def agent_pool_name(name, pool_name), do: Module.concat([name, AgentPool, pool_name]) @doc false @spec partition_key(term(), partition() | nil) :: term() def partition_key(value, nil), do: value def partition_key(value, partition), do: {:partition, partition, value} @doc false @spec unwrap_partition_key(term()) :: {partition() | nil, term()} def unwrap_partition_key({:partition, partition, value}), do: {partition, value} def unwrap_partition_key(value), do: {nil, value} # --------------------------------------------------------------------------- # Agent Lifecycle # --------------------------------------------------------------------------- @doc """ Starts an agent under a specific Jido instance. ## Examples {:ok, pid} = Jido.start_agent(MyApp.Jido, MyAgent) {:ok, pid} = Jido.start_agent(MyApp.Jido, MyAgent, id: "custom-id") """ @spec start_agent(atom(), module() | struct(), keyword()) :: DynamicSupervisor.on_start_child() def start_agent(jido_instance, agent, opts \\ []) when is_atom(jido_instance) do child_spec = {Jido.AgentServer, Keyword.merge(opts, agent: agent, jido: jido_instance)} DynamicSupervisor.start_child(agent_supervisor_name(jido_instance), child_spec) end @doc """ Stops an agent by pid or id. ## Examples :ok = Jido.stop_agent(MyApp.Jido, pid) :ok = Jido.stop_agent(MyApp.Jido, "agent-id") """ @spec stop_agent(atom(), pid() | String.t()) :: :ok | {:error, :not_found} def stop_agent(jido_instance, pid) when is_atom(jido_instance) and is_pid(pid) do DynamicSupervisor.terminate_child(agent_supervisor_name(jido_instance), pid) end def stop_agent(jido_instance, id) when is_atom(jido_instance) and is_binary(id) do case whereis(jido_instance, id) do nil -> {:error, :not_found} pid -> stop_agent(jido_instance, pid) end end @spec stop_agent(atom(), pid() | String.t(), keyword()) :: :ok | {:error, :not_found} def stop_agent(jido_instance, pid, _opts) when is_atom(jido_instance) and is_pid(pid) do stop_agent(jido_instance, pid) end def stop_agent(jido_instance, id, opts) when is_atom(jido_instance) and is_binary(id) and is_list(opts) do case whereis(jido_instance, id, opts) do nil -> {:error, :not_found} pid -> stop_agent(jido_instance, pid) end end @doc """ Looks up an agent by ID in a Jido instance's registry. Returns the pid if found, nil otherwise. ## Examples pid = Jido.whereis(MyApp.Jido, "agent-123") """ @spec whereis(atom(), String.t()) :: pid() | nil def whereis(jido_instance, id) when is_atom(jido_instance) and is_binary(id) do whereis(jido_instance, id, []) end @spec whereis(atom(), String.t(), keyword()) :: pid() | nil def whereis(jido_instance, id, opts) when is_atom(jido_instance) and is_binary(id) and is_list(opts) do registry_key = partition_key(id, Keyword.get(opts, :partition)) case Registry.lookup(registry_name(jido_instance), registry_key) do [{pid, _}] -> pid [] -> nil end end @doc """ Fetches the persisted logical parent binding for a child agent. This is the stable runtime relationship lookup API for orchestration layers that need to inspect the live parent/child graph without depending on raw `RuntimeStore` hive layout. Returns `{:ok, binding}` when present, or `:error` when no binding exists. ## Examples {:ok, binding} = Jido.parent_binding(MyApp.Jido, "child-123") assert binding.parent_id == "parent-456" """ @spec parent_binding(atom(), String.t()) :: {:ok, map()} | :error def parent_binding(jido_instance, child_id) when is_atom(jido_instance) and is_binary(child_id) do parent_binding(jido_instance, child_id, []) end @spec parent_binding(atom(), String.t(), keyword()) :: {:ok, map()} | :error def parent_binding(jido_instance, child_id, opts) when is_atom(jido_instance) and is_binary(child_id) and is_list(opts) do case RuntimeStore.fetch( jido_instance, :relationships, partition_key(child_id, Keyword.get(opts, :partition)) ) do {:ok, binding} -> normalize_parent_binding(binding) :error -> :error end end @doc """ Lists all agents running in a Jido instance. Returns a list of `{id, pid}` tuples. ## Examples agents = Jido.list_agents(MyApp.Jido) # => [{"agent-1", #PID<0.123.0>}, {"agent-2", #PID<0.124.0>}] """ @spec list_agents(atom()) :: [{String.t(), pid()}] def list_agents(jido_instance) when is_atom(jido_instance) do list_agents(jido_instance, []) end @spec list_agents(atom(), keyword()) :: [{String.t(), pid()}] def list_agents(jido_instance, opts) when is_atom(jido_instance) and is_list(opts) do registry_name(jido_instance) |> Registry.select([{{:"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]) |> filter_agent_registry_entries(Keyword.get(opts, :partition)) end @doc """ Returns the count of running agents in a Jido instance. ## Examples count = Jido.agent_count(MyApp.Jido) # => 5 """ @spec agent_count(atom()) :: non_neg_integer() def agent_count(jido_instance) when is_atom(jido_instance) do agent_count(jido_instance, []) end @spec agent_count(atom(), keyword()) :: non_neg_integer() def agent_count(jido_instance, opts) when is_atom(jido_instance) and is_list(opts) do jido_instance |> list_agents(opts) |> length() end # --------------------------------------------------------------------------- # Persistence # --------------------------------------------------------------------------- @doc "Hibernate an agent using the given Jido instance." @spec hibernate(atom(), Jido.Agent.t()) :: :ok | {:error, term()} def hibernate(jido_instance, agent) when is_atom(jido_instance) do hibernate(jido_instance, agent, []) end @spec hibernate(atom(), Jido.Agent.t(), keyword()) :: :ok | {:error, term()} def hibernate(jido_instance, agent, opts) when is_atom(jido_instance) and is_list(opts) do with {:ok, agent} <- maybe_put_partition(agent, Keyword.get(opts, :partition)) do Jido.Persist.hibernate(jido_instance, agent) end end @doc "Thaw an agent using the given Jido instance." @spec thaw(atom(), module(), term()) :: {:ok, Jido.Agent.t()} | {:error, term()} def thaw(jido_instance, agent_module, key) when is_atom(jido_instance) do thaw(jido_instance, agent_module, key, []) end @spec thaw(atom(), module(), term(), keyword()) :: {:ok, Jido.Agent.t()} | {:error, term()} def thaw(jido_instance, agent_module, key, opts) when is_atom(jido_instance) and is_list(opts) do partition = Keyword.get(opts, :partition) jido_instance |> Jido.Persist.thaw(agent_module, partition_key(key, partition)) |> case do {:ok, agent} -> maybe_put_partition(agent, partition) {:error, _reason} = error -> error end end defp filter_agent_registry_entries(entries, partition) do Enum.flat_map(entries, fn {registry_key, pid} -> case unwrap_partition_key(registry_key) do {^partition, id} when is_binary(id) -> [{id, pid}] {nil, id} when is_nil(partition) and is_binary(id) -> [{id, pid}] _other -> [] end end) end defp maybe_put_partition(%{state: state} = agent, nil) when is_map(state), do: {:ok, agent} defp maybe_put_partition(%{state: state} = agent, partition) when is_map(state) do case Map.get(state, :__partition__) do nil -> {:ok, %{agent | state: Map.put(state, :__partition__, partition)}} ^partition -> {:ok, agent} agent_partition -> {:error, Jido.Error.validation_error("partition does not match agent runtime state", %{ partition: partition, agent_partition: agent_partition })} end end defp maybe_put_partition(agent, _partition), do: {:ok, agent} defp normalize_parent_binding(%{parent_id: parent_id, tag: _tag} = binding) when is_binary(parent_id) do {:ok, binding |> Map.put_new(:parent_partition, nil) |> Map.update(:meta, %{}, fn meta when is_map(meta) -> meta _other -> %{} end)} end defp normalize_parent_binding(_binding), do: :error # --------------------------------------------------------------------------- # Discovery # --------------------------------------------------------------------------- @doc "Lists discovered Actions with optional filtering." defdelegate list_actions(opts \\ []), to: Jido.Discovery @doc "Lists discovered Sensors with optional filtering." defdelegate list_sensors(opts \\ []), to: Jido.Discovery @doc "Lists discovered Plugins with optional filtering." defdelegate list_plugins(opts \\ []), to: Jido.Discovery @doc "Lists discovered Demos with optional filtering." defdelegate list_demos(opts \\ []), to: Jido.Discovery @doc "Gets an Action by its slug." defdelegate get_action_by_slug(slug), to: Jido.Discovery @doc "Gets a Sensor by its slug." defdelegate get_sensor_by_slug(slug), to: Jido.Discovery @doc "Gets a Plugin by its slug." defdelegate get_plugin_by_slug(slug), to: Jido.Discovery @doc "Refreshes the Discovery catalog." defdelegate refresh_discovery(), to: Jido.Discovery, as: :refresh # --------------------------------------------------------------------------- # Agent Coordination # --------------------------------------------------------------------------- @doc """ Wait for an agent to reach a terminal status. See `Jido.Await.completion/3` for details. """ defdelegate await(server, timeout_ms \\ Defaults.await_timeout_ms(), opts \\ []), to: Jido.Await, as: :completion @doc """ Wait for a child agent to reach a terminal status. See `Jido.Await.child/4` for details. """ defdelegate await_child( server, child_tag, timeout_ms \\ Defaults.await_child_timeout_ms(), opts \\ [] ), to: Jido.Await, as: :child @doc """ Wait for all agents to reach terminal status. See `Jido.Await.all/3` for details. """ defdelegate await_all(servers, timeout_ms \\ Defaults.await_timeout_ms(), opts \\ []), to: Jido.Await, as: :all @doc """ Wait for any agent to reach terminal status. See `Jido.Await.any/3` for details. """ defdelegate await_any(servers, timeout_ms \\ Defaults.await_timeout_ms(), opts \\ []), to: Jido.Await, as: :any @doc """ Get the PIDs of all children of a parent agent. See `Jido.Await.get_children/1` for details. """ defdelegate get_children(parent_server), to: Jido.Await @doc """ Get a specific child's PID by tag. See `Jido.Await.get_child/2` for details. """ defdelegate get_child(parent_server, child_tag), to: Jido.Await @doc """ Check if an agent process is alive and responding. See `Jido.Await.alive?/1` for details. """ defdelegate alive?(server), to: Jido.Await @doc """ Request graceful cancellation of an agent. See `Jido.Await.cancel/2` for details. """ defdelegate cancel(server, opts \\ []), to: Jido.Await end