defmodule Jido do @moduledoc """ 自動 (Jido) - A foundational framework for building autonomous, distributed agent systems in Elixir. This module provides the main interface for interacting with Jido components, including: - Managing and interacting with Agents through a high-level API - Listing and retrieving Actions, Sensors, and Domains - Filtering and paginating results - Generating unique slugs for components ## Agent Interaction Examples # Find and act on an agent "agent-id" |> Jido.get_agent_by_id() |> Jido.act(:command, %{param: "value"}) # Act asynchronously {:ok, agent} = Jido.get_agent_by_id("agent-id") Jido.act_async(agent, :command) # Send management commands {:ok, agent} = Jido.get_agent_by_id("agent-id") Jido.manage(agent, :pause) # Subscribe to agent events {:ok, topic} = Jido.get_agent_topic("agent-id") Phoenix.PubSub.subscribe(MyApp.PubSub, topic) """ @type component_metadata :: %{ module: module(), name: String.t(), description: String.t(), slug: String.t(), category: atom() | nil, tags: [atom()] | nil } @callback config() :: keyword() defmacro __using__(opts) do quote do @behaviour unquote(__MODULE__) @otp_app unquote(opts)[:otp_app] || raise(ArgumentError, """ You must provide `otp_app: :your_app` to use Jido, e.g.: use Jido, otp_app: :my_app """) # Public function to retrieve config from application environment def config do Application.get_env(@otp_app, __MODULE__, []) |> Keyword.put_new(:agent_registry, Jido.AgentRegistry) end # Get the configured agent registry def agent_registry, do: config()[:agent_registry] # Provide a child spec so we can be placed directly under a Supervisor @spec child_spec(any()) :: Supervisor.child_spec() def child_spec(_arg) do %{ id: __MODULE__, start: {__MODULE__, :start_link, []}, shutdown: 5000, type: :supervisor } end # Entry point for starting the Jido supervisor @spec start_link() :: Supervisor.on_start() def start_link do unquote(__MODULE__).ensure_started(__MODULE__) end # Delegate high-level API methods to Jido module defdelegate cmd(agent, action, args \\ %{}, opts \\ []), to: Jido defdelegate get_agent(id), to: Jido defdelegate get_agent_topic(agent_or_id), to: Jido defdelegate get_agent_status(agent_or_id), to: Jido defdelegate get_agent_supervisor(agent_or_id), to: Jido defdelegate get_agent_state(agent_or_id), to: Jido end end @doc """ Retrieves a running Agent by its ID. ## Parameters - `id`: String or atom ID of the agent to retrieve - `opts`: Optional keyword list of options: - `:registry`: Override the default agent registry ## Returns - `{:ok, pid}` if agent is found and running - `{:error, :not_found}` if agent doesn't exist ## Examples iex> {:ok, agent} = Jido.get_agent("my-agent") {:ok, #PID<0.123.0>} # Using a custom registry iex> {:ok, agent} = Jido.get_agent("my-agent", registry: MyApp.Registry) {:ok, #PID<0.123.0>} """ @spec get_agent(String.t() | atom(), keyword()) :: {:ok, pid()} | {:error, :not_found} def get_agent(id, opts \\ []) when is_binary(id) or is_atom(id) do registry = opts[:registry] || Jido.AgentRegistry case Registry.lookup(registry, id) do [{pid, _}] -> {:ok, pid} [] -> {:error, :not_found} end end @doc """ Pipe-friendly version of get_agent that raises on errors. ## Parameters - `id`: String or atom ID of the agent to retrieve - `opts`: Optional keyword list of options: - `:registry`: Override the default agent registry ## Returns - `pid` if agent is found - Raises `RuntimeError` if agent not found ## Examples iex> "my-agent" |> Jido.get_agent!() |> Jido.cmd(:command) :ok """ @spec get_agent!(String.t() | atom(), keyword()) :: pid() def get_agent!(id, opts \\ []) do case get_agent(id, opts) do {:ok, pid} -> pid {:error, :not_found} -> raise "Agent not found: #{id}" end end @doc """ Sends a command to an agent. ## Parameters - `agent`: Agent pid or return value from get_agent - `action`: The action to execute - `args`: Optional map of action arguments - `opts`: Optional keyword list of options ## Returns Returns the result of command execution. ## Examples iex> {:ok, agent} = Jido.get_agent("my-agent") iex> Jido.cmd(agent, :generate_response, %{prompt: "Hello"}) {:ok, %{response: "Hi there!"}} """ @spec cmd(pid() | {:ok, pid()}, atom(), map(), keyword()) :: any() def cmd(pid_or_tuple, action \\ :default, args \\ %{}, opts \\ []) def cmd({:ok, pid}, action, args, opts), do: cmd(pid, action, args, opts) def cmd(pid, action, args, opts) when is_pid(pid) do Jido.Agent.Server.cmd(pid, action, args, opts) end @doc """ Gets the PubSub topic for an agent. ## Parameters - `agent_or_id`: Agent pid, ID, or return value from get_agent ## Returns - `{:ok, topic}` with the agent's topic string - `{:error, reason}` if topic couldn't be retrieved ## Examples iex> {:ok, topic} = Jido.get_agent_topic("my-agent") {:ok, "jido.agent.my-agent"} iex> {:ok, agent} = Jido.get_agent("my-agent") iex> {:ok, topic} = Jido.get_agent_topic(agent) {:ok, "jido.agent.my-agent"} """ @spec get_agent_topic(pid() | {:ok, pid()} | String.t()) :: {:ok, String.t()} | {:error, term()} def get_agent_topic({:ok, pid}), do: get_agent_topic(pid) def get_agent_topic(pid) when is_pid(pid) do Jido.Agent.Server.get_topic(pid) end def get_agent_topic(id) when is_binary(id) or is_atom(id) do case get_agent(id) do {:ok, pid} -> get_agent_topic(pid) error -> error end end @doc """ Gets the status of an agent. ## Parameters - `agent_or_id`: Agent pid, ID, or return value from get_agent ## Returns - `{:ok, status}` with the agent's status - `{:error, reason}` if status couldn't be retrieved ## Examples iex> {:ok, status} = Jido.get_agent_status("my-agent") {:ok, :idle} """ @spec get_agent_status(pid() | {:ok, pid()} | String.t()) :: {:ok, atom()} | {:error, term()} def get_agent_status({:ok, pid}), do: get_agent_status(pid) def get_agent_status(pid) when is_pid(pid) do Jido.Agent.Server.get_status(pid) end def get_agent_status(id) when is_binary(id) or is_atom(id) do case get_agent(id) do {:ok, pid} -> get_agent_status(pid) error -> error end end @doc """ Gets the supervisor for an agent. ## Parameters - `agent_or_id`: Agent pid, ID, or return value from get_agent ## Returns - `{:ok, supervisor_pid}` with the agent's supervisor pid - `{:error, reason}` if supervisor couldn't be retrieved ## Examples iex> {:ok, supervisor} = Jido.get_agent_supervisor("my-agent") {:ok, #PID<0.124.0>} """ @spec get_agent_supervisor(pid() | {:ok, pid()} | String.t()) :: {:ok, pid()} | {:error, term()} def get_agent_supervisor({:ok, pid}), do: get_agent_supervisor(pid) def get_agent_supervisor(pid) when is_pid(pid) do Jido.Agent.Server.get_supervisor(pid) end def get_agent_supervisor(id) when is_binary(id) or is_atom(id) do case get_agent(id) do {:ok, pid} -> get_agent_supervisor(pid) error -> error end end @doc """ Gets the current state of an agent. ## Parameters - `agent_or_id`: Agent pid, ID, or return value from get_agent ## Returns - `{:ok, state}` with the agent's current state - `{:error, reason}` if state couldn't be retrieved ## Examples iex> {:ok, state} = Jido.get_agent_state("my-agent") {:ok, %Jido.Agent.Server.State{...}} """ @spec get_agent_state(pid() | {:ok, pid()} | String.t()) :: {:ok, term()} | {:error, term()} def get_agent_state({:ok, pid}), do: get_agent_state(pid) def get_agent_state(pid) when is_pid(pid) do Jido.Agent.Server.get_state(pid) end def get_agent_state(id) when is_binary(id) or is_atom(id) do case get_agent(id) do {:ok, pid} -> get_agent_state(pid) error -> error end end @doc """ Clones an existing agent with a new ID. ## Parameters - `source_id`: ID of the agent to clone - `new_id`: ID for the new cloned agent - `opts`: Optional keyword list of options to override for the new agent ## Returns - `{:ok, pid}` with the new agent's process ID - `{:error, reason}` if cloning fails ## Examples iex> {:ok, new_pid} = Jido.clone_agent("source-agent", "cloned-agent") {:ok, #PID<0.125.0>} """ @spec clone_agent(String.t() | atom(), String.t() | atom(), keyword()) :: {:ok, pid()} | {:error, term()} def clone_agent(source_id, new_id, opts \\ []) do with {:ok, source_pid} <- get_agent(source_id), {:ok, source_state} <- Jido.Agent.Server.get_state(source_pid) do # Create new agent with updated ID but same config agent = %{source_state.agent | id: new_id} # Merge original options with any overrides new_opts = opts |> Keyword.merge( agent: agent, pubsub: source_state.pubsub, # Let server generate new topic topic: nil, max_queue_size: source_state.max_queue_size ) Jido.Agent.Server.start_link(new_opts) end end @doc """ Callback used by the generated `start_link/0` function. This is where we actually call Jido.Supervisor.start_link. """ @spec ensure_started(module()) :: Supervisor.on_start() def ensure_started(jido_module) do config = jido_module.config() Jido.Supervisor.start_link(jido_module, config) end @doc """ Retrieves a prompt file from the priv/prompts directory by its name. ## Parameters - `name`: An atom representing the name of the prompt file (without .txt extension) ## Returns The contents of the prompt file as a string if found, otherwise raises an error. ## Examples iex> Jido.prompt(:system) "You are a helpful AI assistant..." iex> Jido.prompt(:nonexistent) ** (File.Error) could not read file priv/prompts/nonexistent.txt """ @spec prompt(atom()) :: String.t() def prompt(name) when is_atom(name) do app = Application.get_application(__MODULE__) path = :code.priv_dir(app) prompt_path = Path.join([path, "prompts", "#{name}.txt"]) File.read!(prompt_path) end # Component Discovery defdelegate list_actions(opts \\ []), to: Jido.Discovery defdelegate list_sensors(opts \\ []), to: Jido.Discovery defdelegate list_agents(opts \\ []), to: Jido.Discovery defdelegate get_action_by_slug(slug), to: Jido.Discovery defdelegate get_sensor_by_slug(slug), to: Jido.Discovery defdelegate get_agent_by_slug(slug), to: Jido.Discovery end