{"id":45301351,"url":"https://github.com/sagents-ai/sagents","last_synced_at":"2026-05-08T00:02:19.345Z","repository":{"id":337745359,"uuid":"1149341400","full_name":"sagents-ai/sagents","owner":"sagents-ai","description":"Build interactive AI agents in Elixir with OTP supervision, middleware composition, human-in-the-loop approvals, sub-agent delegation, and real-time Phoenix LiveView integration. Built on LangChain.","archived":false,"fork":false,"pushed_at":"2026-05-06T05:06:47.000Z","size":1774,"stargazers_count":223,"open_issues_count":0,"forks_count":22,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-05-06T07:10:48.016Z","etag":null,"topics":["agent","agentic","ai","elixir","hitl","human-in-the-loop","llm","middleware"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sagents-ai.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-04T02:11:49.000Z","updated_at":"2026-05-06T05:47:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/sagents-ai/sagents","commit_stats":null,"previous_names":["sagents-ai/sagents"],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/sagents-ai/sagents","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sagents-ai%2Fsagents","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sagents-ai%2Fsagents/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sagents-ai%2Fsagents/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sagents-ai%2Fsagents/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sagents-ai","download_url":"https://codeload.github.com/sagents-ai/sagents/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sagents-ai%2Fsagents/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32760962,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","agentic","ai","elixir","hitl","human-in-the-loop","llm","middleware"],"created_at":"2026-02-21T05:01:08.160Z","updated_at":"2026-05-08T00:02:19.322Z","avatar_url":"https://github.com/sagents-ai.png","language":"Elixir","funding_links":[],"categories":["Agent Frameworks","Generative AI"],"sub_categories":["Agent Frameworks"],"readme":"# Sagents\n\n\u003e **Sage Agents** - Combining the wisdom of a [Sage](https://en.wikipedia.org/wiki/Sage_(philosophy)) with the power of LLM-based Agents\n\nA sage is a person who has attained wisdom and is often characterized by sound judgment and deep understanding. Sagents brings this philosophy to AI agents: building systems that don't just execute tasks, but do so with thoughtful human oversight, efficient resource management, and extensible architecture.\n\n## Key Features\n\n- **Human-In-The-Loop (HITL)** - Customizable permission system that pauses execution for approval on sensitive operations, including parallel tool calls where each action can be individually approved/rejected. Works across both main agents and SubAgents — interrupts propagate up to the parent for approval and resume seamlessly\n- **Composable Execution Modes** - Agent run loops are explicit Elixir pipelines built from reusable steps. Mix and match built-in steps (`call_llm`, `execute_tools`, `check_pre_tool_hitl`, `propagate_state`, etc.) or write your own. Different agents can use different modes in the same application\n- **Structured Agent Completion (`until_tool`)** - Force agents to loop until they call a specific tool, returning the result as a clean `{:ok, state, %ToolResult{}}` tuple. No more hoping the LLM follows your output format — get structured data you can pattern match on\n- **SubAgents** - Delegate complex tasks to specialized child agents for efficient context management and parallel execution\n- **GenServer Architecture** - Each agent runs as a supervised OTP process with automatic lifecycle management\n- **Phoenix.Presence Integration** - Smart resource management that knows when to shut down idle agents\n- **PubSub Real-Time Events** - Stream agent state, messages, and events to multiple LiveView subscribers\n- **Middleware System** - Extensible plugin architecture for adding capabilities to agents, including composable observability callbacks for OpenTelemetry, metrics, or custom logging\n- **Cluster-Aware Distribution** - Optional Horde-based distribution for running agents across a cluster of nodes with automatic state migration, or run locally on a single node (the default)\n- **State Persistence** - Save and restore agent conversations via optional behaviour modules for agent state and display messages\n- **Virtual Filesystem** - Isolated, in-memory file operations with optional persistence\n\n**See it in action!** Try the [agents_demo](https://github.com/sagents-ai/agents_demo) application to experience Sagents interactively, or add the [sagents_live_debugger](https://github.com/sagents-ai/sagents_live_debugger) to your app for real-time insights into agent configuration, state, and event flows.\n\n![AgentsDemo Chat Interface](https://raw.githubusercontent.com/sagents-ai/sagents/refs/heads/main/screenshots/AgentsDemo-Files.png)\n\n*The [AgentsDemo](https://github.com/sagents-ai/agents_demo) chat interface showing the use of a virtual filesystem, tool call execution, composable middleware, supervised Agentic GenServer assistant, and much more!*\n\n## Who Is This For?\n\nSagents is designed for Elixir developers building **interactive AI applications** where:\n\n- Users have real-time conversations with AI agents\n- Human oversight is required for certain operations (file deletes, API calls, etc.)\n- Multiple concurrent conversations need isolated agent processes\n- Agent state must persist across sessions\n- Real-time UI updates are essential (Phoenix LiveView)\n\nIf you're building a simple CLI tool or batch processing pipeline, the core [LangChain](https://github.com/brainlid/langchain) library may be sufficient. Sagents adds the orchestration layer needed for production interactive applications.\n\n**What about non-interactive agents?** Certainly! Sagents works perfectly well for background agents without a UI. You'd simply skip the UI state management helpers and omit middleware like HumanInTheLoop. The agent still runs as a supervised GenServer with all the benefits of state persistence, middleware capabilities, and SubAgent delegation. The [sagents_live_debugger](https://github.com/sagents-ai/sagents_live_debugger) package remains valuable for gaining visibility into what your agents are doing, even without an end-user interface.\n\n## Installation\n\nAdd `sagents` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:sagents, \"~\u003e 0.7.0\"}\n  ]\nend\n```\n\nLangChain is automatically included as a dependency.\n\n### Supervision Tree Setup\n\nAdd `Sagents.Supervisor` to your application's supervision tree:\n\n```elixir\n# lib/my_app/application.ex\nchildren = [\n  # ... your other children (Repo, PubSub, etc.)\n  Sagents.Supervisor\n]\n```\n\nThis starts the process registry, dynamic supervisors, and filesystem supervisor that Sagents uses to manage agents.\n\n## Configuration\n\nSagents builds on the [Elixir LangChain](https://github.com/brainlid/langchain) library for LLM integration. To use Sagents, you need to configure an LLM provider by setting the appropriate API key as an environment variable:\n\n```bash\n# For Anthropic (Claude)\nexport ANTHROPIC_API_KEY=\"your-api-key\"\n\n# For OpenAI (GPT)\nexport OPENAI_API_KEY=\"your-api-key\"\n\n# For Google (Gemini)\nexport GOOGLE_API_KEY=\"your-api-key\"\n```\n\nThen specify the model when creating your agent:\n\n```elixir\n# Anthropic Claude\nalias LangChain.ChatModels.ChatAnthropic\nmodel = ChatAnthropic.new!(%{model: \"claude-sonnet-4-5-20250929\"})\n\n# OpenAI GPT\nalias LangChain.ChatModels.ChatOpenAI\nmodel = ChatOpenAI.new!(%{model: \"gpt-4o\"})\n\n# Google Gemini\nalias LangChain.ChatModels.ChatGoogleAI\nmodel = ChatGoogleAI.new!(%{model: \"gemini-2.0-flash-exp\"})\n```\n\nFor detailed configuration options, start here [LangChain documentation](https://hexdocs.pm/langchain/readme.html#installation).\n\n## Quick Start\n\n### 1. Create an Agent\n\n```elixir\nalias Sagents.{Agent, AgentServer, State}\nalias Sagents.Middleware.{TodoList, FileSystem, HumanInTheLoop}\nalias LangChain.ChatModels.ChatAnthropic\nalias LangChain.Message\n\n# Create agent with middleware capabilities\n{:ok, agent} = Agent.new(%{\n  agent_id: \"my-agent-1\",\n  model: ChatAnthropic.new!(%{model: \"claude-sonnet-4-5-20250929\"}),\n  base_system_prompt: \"You are a helpful coding assistant.\",\n  middleware: [\n    TodoList,\n    FileSystem,\n    {HumanInTheLoop, [\n      interrupt_on: %{\n        \"write_file\" =\u003e true,\n        \"delete_file\" =\u003e true\n      }\n    ]}\n  ]\n})\n```\n\n### 2. Start the AgentServer\n\n```elixir\n# Create initial state\nstate = State.new!(%{\n  messages: [Message.new_user!(\"Create a hello world program\")]\n})\n\n# Start the AgentServer (runs as a supervised GenServer)\n{:ok, _pid} = AgentServer.start_link(\n  agent: agent,\n  initial_state: state,\n  pubsub: {Phoenix.PubSub, :my_app_pubsub},\n  inactivity_timeout: 3_600_000  # 1 hour\n)\n\n# Subscribe to real-time events\nAgentServer.subscribe(\"my-agent-1\")\n\n# Execute the agent\n:ok = AgentServer.execute(\"my-agent-1\")\n```\n\n### 3. Handle Events\n\n```elixir\n# In your LiveView or GenServer\ndef handle_info({:agent, event}, socket) do\n  case event do\n    {:status_changed, :running, nil} -\u003e\n      # Agent started processing\n      {:noreply, assign(socket, status: :running)}\n\n    {:llm_deltas, deltas} -\u003e\n      # Streaming tokens received\n      {:noreply, stream_tokens(socket, deltas)}\n\n    {:llm_message, message} -\u003e\n      # Complete message received\n      {:noreply, add_message(socket, message)}\n\n    {:todos_updated, todos} -\u003e\n      # Agent's TODO list changed\n      {:noreply, assign(socket, todos: todos)}\n\n    {:status_changed, :interrupted, interrupt_data} -\u003e\n      # Human approval needed\n      {:noreply, show_approval_dialog(socket, interrupt_data)}\n\n    {:status_changed, :idle, nil} -\u003e\n      # Agent completed\n      {:noreply, assign(socket, status: :idle)}\n\n    {:agent_shutdown, metadata} -\u003e\n      # Agent shutting down (inactivity or no viewers)\n      {:noreply, handle_shutdown(socket, metadata)}\n  end\nend\n```\n\n### 4. Handle Human-In-The-Loop Approvals\n\n```elixir\n# When agent needs approval, it returns interrupt data\n# User reviews and provides decisions\n\ndecisions = [\n  %{type: :approve},                                    # Approve first tool call\n  %{type: :edit, arguments: %{\"path\" =\u003e \"safe.txt\"}},   # Edit second tool call\n  %{type: :reject}                                      # Reject third tool call\n]\n\n# Resume execution with decisions\n:ok = AgentServer.resume(\"my-agent-1\", decisions)\n```\n\n### 5. Structured Completion with `until_tool`\n\nForce an agent to return structured output by calling a specific tool:\n\n```elixir\n# Agent loops until \"deliver_answer\" is called, then returns the tool result\ncase Agent.execute(agent, state, until_tool: \"deliver_answer\") do\n  {:ok, final_state, %ToolResult{} = result} -\u003e\n    # Structured data from the target tool\n    IO.inspect(result.content)\n\n  {:error, reason} -\u003e\n    # LLM stopped without calling the target tool\n    IO.puts(\"Error: #{inspect(reason)}\")\nend\n```\n\nThis works through HITL interrupt/resume cycles and with SubAgents — the contract is enforced at every level.\n\n## Provided Middleware\n\nSagents includes several pre-built middleware components:\n\n| Middleware | Description |\n|------------|-------------|\n| **TodoList** | Task management with `write_todos` tool for tracking multi-step work |\n| **FileSystem** | Virtual filesystem with `ls`, `read_file`, `write_file`, `edit_file`, `find_in_file`, `edit_lines`, `delete_file` |\n| **HumanInTheLoop** | Pause execution for human approval on configurable tools |\n| **SubAgent** | Delegate tasks to specialized child agents for parallel execution |\n| **Summarization** | Automatic conversation compression when token limits approach |\n| **PatchToolCalls** | Fix dangling tool calls from interrupted conversations |\n| **ConversationTitle** | Auto-generate conversation titles from first user message |\n| **DebugLog** | Local dev tool — writes per-conversation structured logs (messages, tool calls, state changes) to dedicated files for debugging |\n| **ProcessContext** | Propagates caller-process state (OpenTelemetry trace context, Sentry context, tenant scope, request-scoped logger metadata) across the Caller → AgentServer → chain Task → per-tool async Task boundaries |\n\n### FileSystem Middleware\n\n```elixir\n{:ok, agent} = Agent.new(%{\n  # ...\n  middleware: [\n    {FileSystem, [\n      enabled_tools: [\"ls\", \"read_file\", \"write_file\", \"edit_file\"],\n      # Optional: persistence callbacks\n      persistence: MyApp.FilePersistence,\n      context: %{user_id: current_user.id}\n    ]}\n  ]\n})\n```\n\n### SubAgent Middleware\n\nSubAgents provide efficient context management by isolating complex tasks:\n\n```elixir\n{:ok, agent} = Agent.new(%{\n  # ...\n  middleware: [\n    {SubAgent, [\n      model: ChatAnthropic.new!(%{model: \"claude-sonnet-4-5-20250929\"}),\n      subagents: [\n        SubAgent.Config.new!(%{\n          name: \"researcher\",\n          description: \"Research topics using web search\",\n          system_prompt: \"You are an expert researcher...\",\n          tools: [web_search_tool]\n        }),\n        SubAgent.Compiled.new!(%{\n          name: \"coder\",\n          description: \"Write and review code\",\n          agent: pre_built_coder_agent\n        })\n      ],\n      # Prevent recursive SubAgent nesting\n      block_middleware: [ConversationTitle, Summarization]\n    ]}\n  ]\n})\n```\n\nSubAgents also respect HITL permissions - if a SubAgent attempts a protected operation, the interrupt propagates to the parent for approval.\n\n### Human-In-The-Loop Middleware\n\nConfigure which tools require human approval:\n\n```elixir\n{HumanInTheLoop, [\n  interrupt_on: %{\n    # Simple boolean\n    \"write_file\" =\u003e true,\n    \"delete_file\" =\u003e true,\n\n    # Advanced: customize allowed decisions\n    \"execute_command\" =\u003e %{\n      allowed_decisions: [:approve, :reject]  # No edit option\n    }\n  }\n]}\n```\n\nDecision types:\n- `:approve` - Execute with original arguments\n- `:edit` - Execute with modified arguments\n- `:reject` - Skip execution, inform agent of rejection\n\n## Custom Middleware\n\nCreate your own middleware by implementing the `Sagents.Middleware` behaviour:\n\n```elixir\ndefmodule MyApp.CustomMiddleware do\n  @behaviour Sagents.Middleware\n\n  @impl true\n  def init(opts) do\n    config = %{\n      enabled: Keyword.get(opts, :enabled, true)\n    }\n    {:ok, config}\n  end\n\n  @impl true\n  def system_prompt(_config) do\n    \"You have access to custom capabilities.\"\n  end\n\n  @impl true\n  def tools(config) do\n    [my_custom_tool(config)]\n  end\n\n  @impl true\n  def before_model(state, _config) do\n    # Preprocess state before LLM call\n    {:ok, state}\n  end\n\n  @impl true\n  def after_model(state, _config) do\n    # Postprocess state after LLM response\n    # Return {:interrupt, state, interrupt_data} to pause for HITL\n    {:ok, state}\n  end\n\n  @impl true\n  def handle_message(message, state, _config) do\n    # Handle async messages from spawned tasks\n    {:ok, state}\n  end\n\n  @impl true\n  def on_server_start(state, _config) do\n    # Called when AgentServer starts - broadcast initial state\n    {:ok, state}\n  end\nend\n```\n\n## Custom Execution Modes\n\nAgent execution in Sagents is an explicit pipeline, not a black box. The default mode composes built-in steps from both LangChain and Sagents:\n\n```elixir\n# This is the entire default run loop (Sagents.Modes.AgentExecution)\ndefp do_run(chain, opts) do\n  {:continue, chain}\n  |\u003e call_llm()\n  |\u003e check_max_runs(Keyword.put_new(opts, :max_runs, 50))\n  |\u003e check_pause(opts)\n  |\u003e check_pre_tool_hitl(opts)\n  |\u003e execute_tools()\n  |\u003e propagate_state(opts)\n  |\u003e check_tool_interrupts(opts)\n  |\u003e maybe_check_until_tool(opts)\n  |\u003e continue_or_done_safe(\u0026do_run/2, opts)\nend\n```\n\nEvery step follows a simple contract: `{:continue, chain}` means keep going, any other tuple (`:ok`, `:error`, `:interrupt`, `:pause`) is terminal and passes through unchanged. Write your own steps using the same pattern and drop them into the pipeline.\n\n### Creating a Custom Mode\n\n```elixir\ndefmodule MyApp.Modes.Simple do\n  @behaviour LangChain.Chains.LLMChain.Mode\n  import LangChain.Chains.LLMChain.Mode.Steps\n\n  @impl true\n  def run(chain, opts) do\n    chain = ensure_mode_state(chain)\n\n    {:continue, chain}\n    |\u003e call_llm()\n    |\u003e execute_tools()\n    |\u003e check_max_runs(opts)\n    |\u003e continue_or_done(\u0026run/2, opts)\n  end\nend\n```\n\nAssign a mode per agent — one agent can be a strict tool-caller, another fully conversational:\n\n```elixir\n{:ok, agent} = Agent.new(%{\n  model: model,\n  mode: MyApp.Modes.Simple,\n  # ...\n})\n```\n\n### Available Built-In Steps\n\n| Step | Source | What it does |\n|------|--------|-------------|\n| `call_llm()` | LangChain | Single LLM call, tracks run count |\n| `execute_tools()` | LangChain | Execute pending tool calls |\n| `check_max_runs(opts)` | LangChain | Safety limit on LLM calls |\n| `check_pause(opts)` | LangChain | Infrastructure drain / node migration |\n| `check_until_tool(opts)` | LangChain | Terminate when target tool is called |\n| `check_tool_interrupts(opts)` | LangChain | Detect tool-level interrupts |\n| `continue_or_done(run_fn, opts)` | LangChain | Loop or return |\n| `check_pre_tool_hitl(opts)` | Sagents | HITL approval check before tool execution |\n| `propagate_state(opts)` | Sagents | Merge tool result state deltas back |\n| `continue_or_done_safe(run_fn, opts)` | Sagents | Loop or return, with `until_tool` enforcement |\n\n## Quick Setup\n\nSagents provides generators to scaffold everything you need for conversation-centric agents:\n\n```bash\nmix sagents.setup MyApp.Conversations \\\n  --scope MyApp.Accounts.Scope \\\n  --owner-type user \\\n  --owner-field user_id\n```\n\nThis generates:\n- **Persistence layer** - Database schemas and migration\n- **Factory module** - Agent creation with model/middleware configuration\n- **Coordinator module** - Session management and lifecycle orchestration\n\nAll configured to work together seamlessly based on your `--owner-type` and `--owner-field` settings.\n\n### What Gets Generated\n\nThe `mix sagents.setup` command creates a complete conversation infrastructure:\n\n#### 1. Persistence Layer\n- **Context module** (`MyApp.Conversations`) with CRUD operations\n- **Schemas**: Conversation, AgentState, DisplayMessage\n- **Database migration** for all tables\n\n#### 2. Factory Module\nCentralizes agent creation at `MyApp.Agents.Factory` with:\n- Model configuration (ChatAnthropic by default, with fallback examples)\n- Default middleware stack (TodoList, FileSystem, SubAgent, Summarization, etc.)\n- Human-in-the-Loop configuration\n- Automatic filesystem scope extraction based on your owner type/field\n\n**Key functions to customize:**\n- `get_model_config/0` - Change LLM provider (OpenAI, Ollama, etc.)\n- `get_fallback_models/0` - Configure model fallbacks for resilience\n- `base_system_prompt/0` - Define your agent's personality and capabilities\n- `build_middleware/3` - Add/remove middleware from the stack\n- `default_interrupt_on/0` - Configure which tools require human approval\n- `get_filesystem_scope/1` - Customize filesystem scoping strategy\n\n#### 3. Coordinator Module\nManages agent lifecycles at `MyApp.Agents.Coordinator` with:\n- Conversation ID → Agent ID mapping\n- On-demand agent starting with idempotent session management\n- State loading from your Conversations context\n- Race condition handling for concurrent starts\n- Phoenix.Presence integration for viewer tracking\n\n**Key functions to customize:**\n- `conversation_agent_id/1` - Change the agent_id mapping strategy\n- `create_conversation_state/1` - Customize state loading behavior\n\n### LiveView Helpers Generator\n\nFor Phoenix LiveView integration, generate a helpers module with reusable handlers for all agent events:\n\n```bash\nmix sagents.gen.live_helpers MyAppWeb.AgentLiveHelpers \\\n  --context MyApp.Conversations\n```\n\nThis generates a module with handler functions that follow the LiveView socket-in/socket-out pattern:\n\n- **Status handlers** - `handle_status_running/1`, `handle_status_idle/1`, `handle_status_cancelled/1`, `handle_status_error/2`, `handle_status_interrupted/2`\n- **Message handlers** - `handle_llm_deltas/2`, `handle_llm_message_complete/1`, `handle_display_message_saved/2`\n- **Tool execution handlers** - `handle_tool_call_identified/2`, `handle_tool_execution_started/2`, `handle_tool_execution_completed/3`, `handle_tool_execution_failed/3`\n- **Lifecycle handlers** - `handle_conversation_title_generated/3`, `handle_agent_shutdown/2`\n- **Core helpers** - `persist_agent_state/2`, `reload_messages_from_db/1`, `update_streaming_message/2`\n\nUse them in your LiveView:\n\n```elixir\ndefmodule MyAppWeb.ChatLive do\n  alias MyAppWeb.AgentLiveHelpers\n\n  def handle_info({:agent, {:status_changed, :running, nil}}, socket) do\n    {:noreply, AgentLiveHelpers.handle_status_running(socket)}\n  end\n\n  def handle_info({:agent, {:llm_deltas, deltas}}, socket) do\n    {:noreply, AgentLiveHelpers.handle_llm_deltas(socket, deltas)}\n  end\n\n  def handle_info({:agent, {:status_changed, :idle, _data}}, socket) do\n    {:noreply, AgentLiveHelpers.handle_status_idle(socket)}\n  end\nend\n```\n\n**Options:**\n- `--context` (required) - Your conversations context module\n- `--test-path` - Custom test file directory (default: inferred from module path)\n- `--no-test` - Skip generating the test file\n\n### Advanced Options\n\n```bash\nmix sagents.setup MyApp.Conversations \\\n  --scope MyApp.Accounts.Scope \\\n  --owner-type user \\\n  --owner-field user_id \\\n  --factory MyApp.Agents.Factory \\\n  --coordinator MyApp.Agents.Coordinator \\\n  --pubsub MyApp.PubSub \\\n  --presence MyAppWeb.Presence \\\n  --table-prefix sagents_\n```\n\nAll options have sensible defaults based on your context module and Phoenix conventions.\n\nFor a fully customized example, see the [agents_demo](https://github.com/sagents-ai/agents_demo) project.\n\n### Usage Pattern\n\n```elixir\n# Create conversation\n{:ok, conversation} = Conversations.create_conversation(scope, %{title: \"My Chat\"})\n\n# Save state during execution\nstate = AgentServer.export_state(agent_id)\nConversations.save_agent_state(conversation.id, state)\n\n# Restore conversation later\n{:ok, persisted_state} = Conversations.load_agent_state(conversation.id)\n\n# Create agent from code (middleware/tools come from code, not database)\n{:ok, agent} = MyApp.AgentFactory.create_agent(agent_id: \"conv-#{conversation.id}\")\n\n# Start with restored state\n{:ok, pid} = AgentServer.start_link_from_state(\n  persisted_state,\n  agent: agent,\n  agent_id: \"conv-#{conversation.id}\",\n  pubsub: {Phoenix.PubSub, :my_pubsub}\n)\n```\n\n## Agent Lifecycle Management\n\n### Process Architecture\n\nSagents uses a flexible supervision architecture built on OTP principles. Process discovery is handled by `Sagents.ProcessRegistry`, which abstracts over `Registry` (local) and `Horde.Registry` (distributed) based on your distribution config:\n\n```\nSagents.Supervisor (added to your application's supervision tree)\n├── Sagents.ProcessRegistry (Registry or Horde.Registry)\n│   └── Process Discovery via Registry Keys:\n│       ├── {:agent_supervisor, agent_id}\n│       ├── {:agent_server, agent_id}\n│       ├── {:sub_agents_supervisor, agent_id}\n│       └── {:filesystem_server, scope_key}\n│\n├── Sagents.ProcessSupervisor (DynamicSupervisor or Horde.DynamicSupervisor)\n│   ├── AgentSupervisor (\"conversation-1\")\n│   │   ├── AgentServer (registers as {:agent_server, \"conversation-1\"})\n│   │   │   └── Broadcasts on topic: \"agent_server:conversation-1\"\n│   │   └── SubAgentsDynamicSupervisor\n│   │       └── SubAgentServer (temporary child agents)\n│   │\n│   └── AgentSupervisor (\"conversation-2\")\n│       ├── AgentServer\n│       └── SubAgentsDynamicSupervisor\n│\n└── FileSystemSupervisor (independent, flexible scoping)\n    ├── FileSystemServer ({:user, 1})      # User-scoped\n    ├── FileSystemServer ({:user, 2})\n    └── FileSystemServer ({:project, 42})  # Project-scoped\n```\n\n#### Key Design Principles\n\n**Registry-Based Discovery**: All processes register with `Sagents.ProcessRegistry` using structured tuple keys. Process lookup happens through the registry, not supervision tree traversal. In local mode this uses `Registry`; in distributed mode it uses `Horde.Registry` for cluster-wide discovery.\n\n**Dynamic Agent Lifecycle**: AgentSupervisor instances are started on-demand by the Coordinator via `AgentSupervisor.start_link_sync/1`. The `_sync` variant waits for full registration before returning, preventing race conditions when immediately subscribing to agent events.\n\n**Independent Filesystem Scoping**: FileSystemSupervisor is **separate from agent supervision**, allowing flexible lifetime and scope management:\n- User-scoped filesystem shared across multiple conversations\n- Project-scoped filesystem shared across multiple users\n- Organization-scoped filesystem for team collaboration\n- Agents reference filesystems by `scope_key`, not PID\n\n**Supervision Strategy**: Each AgentSupervisor uses `:rest_for_one` strategy:\n- If AgentServer crashes → SubAgentsDynamicSupervisor restarts\n- If SubAgentsDynamicSupervisor crashes → only it restarts\n- All children use `restart: :temporary` (no automatic restart)\n\n### Inactivity Timeout\n\nAgents automatically shut down after inactivity:\n\n```elixir\nAgentServer.start_link(\n  agent: agent,\n  inactivity_timeout: 3_600_000  # 1 hour (default: 5 minutes)\n  # or nil/:infinity to disable\n)\n```\n\n### Presence-Based Shutdown\n\nWith Phoenix.Presence, agents can detect when no clients are viewing and shut down immediately:\n\n```elixir\nAgentServer.start_link(\n  agent: agent,\n  presence_tracking: [\n    enabled: true,\n    presence_module: MyApp.Presence,\n    topic: \"conversation:#{conversation_id}\"\n  ]\n)\n```\n\nWhen an agent completes and no viewers are connected, it shuts down to free resources.\n\n### State Persistence\n\nAgentServer supports optional persistence via two behaviour modules:\n\n- **`Sagents.AgentPersistence`** — Persists agent state snapshots at key lifecycle points (completion, error, interrupt, shutdown, title generation)\n- **`Sagents.DisplayMessagePersistence`** — Persists display messages and tool execution status updates\n\n```elixir\nAgentServer.start_link(\n  agent: agent,\n  initial_state: state,\n  pubsub: {Phoenix.PubSub, :my_app_pubsub},\n  agent_persistence: MyApp.AgentPersistenceImpl,\n  display_message_persistence: MyApp.DisplayMessagePersistenceImpl\n)\n```\n\nThese behaviours are optional — if not configured, agents run without persistence. The `mix sagents.setup` generator creates implementations for you automatically.\n\n### Cluster Distribution\n\nBy default, Sagents runs locally on a single node. For multi-node deployments, enable Horde-based distribution:\n\n```elixir\n# config/config.exs\nconfig :sagents, :distribution, :horde\n```\n\nWhen Horde is enabled, `Sagents.ProcessRegistry` and `Sagents.ProcessSupervisor` automatically switch to `Horde.Registry` and `Horde.DynamicSupervisor`, giving you:\n\n- **Automatic agent redistribution** across cluster nodes\n- **State migration** when nodes join or leave the cluster\n- **Regional clustering** for Fly.io deployments via `Sagents.Horde.ClusterConfig`\n- **Node transfer events** broadcast during redistribution so your UI can react\n\nAgents broadcast `{:agent, {:node_transferring, data}}` and `{:agent, {:node_transferred, data}}` events during Horde redistribution, allowing connected clients to follow their agent to a new node.\n\nNo code changes are needed beyond the config — the `ProcessRegistry` and `ProcessSupervisor` abstractions handle backend switching transparently.\n\n## PubSub Events\n\nAgentServer broadcasts events on topic `\"agent_server:#{agent_id}\"`:\n\n### Status Events\n- `{:agent, {:status_changed, :idle, nil}}` - Ready for work\n- `{:agent, {:status_changed, :running, nil}}` - Executing\n- `{:agent, {:status_changed, :interrupted, interrupt_data}}` - Awaiting approval\n- `{:agent, {:status_changed, :paused, nil}}` - Infrastructure pause (e.g., node draining); state is persisted and the agent can be resumed after restart\n- `{:agent, {:status_changed, :cancelled, nil}}` - Cancelled by user\n- `{:agent, {:status_changed, :error, reason}}` - Execution failed\n\n### Error Events\n- `{:agent, {:chain_error, error}}` - Terminal chain error after all retries and fallbacks are exhausted. Fires alongside `{:status_changed, :error, _}` but carries the raw `LangChain.LangChainError` struct with structured details (see `:on_error` callback in LangChain)\n\n### Message Events\n- `{:agent, {:llm_deltas, [%MessageDelta{}]}}` - Streaming tokens\n- `{:agent, {:llm_message, %Message{}}}` - Complete message\n- `{:agent, {:llm_token_usage, %TokenUsage{}}}` - Token usage info\n- `{:agent, {:display_message_saved, display_message}}` - Message persisted (requires `DisplayMessagePersistence` behaviour)\n\n### Tool Events\n- `{:agent, {:tool_call_identified, tool_info}}` - Tool call detected during streaming\n- `{:agent, {:tool_execution_started, tool_info}}` - Tool execution began\n- `{:agent, {:tool_execution_completed, call_id, tool_result}}` - Tool execution succeeded\n- `{:agent, {:tool_execution_failed, call_id, error}}` - Tool execution failed\n\n### State Events\n- `{:agent, {:todos_updated, todos}}` - TODO list snapshot\n- `{:agent, {:state_restored, new_state}}` - State restored via `update_agent_and_state/3`\n- `{:agent, {:agent_shutdown, metadata}}` - Shutting down\n\n### Node Transfer Events (Horde only)\n- `{:agent, {:node_transferring, data}}` - Agent migrating to another node\n- `{:agent, {:node_transferred, data}}` - Agent migration completed\n\n### Debug Events (separate topic)\n\nSubscribe with `AgentServer.subscribe(agent_id, :debug)`:\n\n- `{:agent, {:debug, {:agent_state_update, state}}}` - Full state snapshot\n- `{:agent, {:debug, {:middleware_action, module, data}}}` - Middleware events\n- `{:agent, {:debug, {:llm_error, error}}}` - Individual LLM API call failure, including transient errors during retries and model fallback attempts. Fires on every failed call even when the chain subsequently recovers — use this for diagnostics rather than user-facing error handling (see `:on_llm_error` callback in LangChain)\n\n## Agent Discovery\n\nFind and inspect running agents:\n\n```elixir\n# List all running agents\nAgentServer.list_running_agents()\n# =\u003e [\"conversation-1\", \"conversation-2\", \"user-42\"]\n\n# Find agents by pattern\nAgentServer.list_agents_matching(\"conversation-*\")\n# =\u003e [\"conversation-1\", \"conversation-2\"]\n\n# Get agent count\nAgentServer.agent_count()\n# =\u003e 3\n\n# Get detailed info\nAgentServer.agent_info(\"conversation-1\")\n# =\u003e %{\n#   agent_id: \"conversation-1\",\n#   pid: #PID\u003c0.1234.0\u003e,\n#   status: :idle,\n#   message_count: 5,\n#   has_interrupt: false\n# }\n```\n\n## Related Projects\n\n### agents_demo\n\nA complete Phoenix LiveView application demonstrating Sagents in action:\n- Multi-conversation support with real-time state persistence\n- Human-in-the-loop approval workflows\n- File system operations with persistence\n- SubAgent delegation patterns\n\n[View agents_demo →](https://github.com/sagents-ai/agents_demo)\n\n### sagents_live_debugger\n\nA Phoenix LiveView dashboard for debugging agent execution in real-time:\n- Agent configuration inspection\n- Live message flow visualization\n- State and event monitoring\n- Middleware action tracking\n\n```elixir\n# Add to your router\nimport SagentsLiveDebugger.Router\n\nscope \"/dev\" do\n  pipe_through :browser\n\n  sagents_live_debugger \"/debug/agents\",\n    coordinator: MyApp.Agents.Coordinator,\n    conversation_provider: \u0026MyApp.list_conversations/0\nend\n```\n\n[View sagents_live_debugger →](https://github.com/sagents-ai/sagents_live_debugger)\n\n## Conversation Architecture\n\nSagents uses a **dual-view pattern** for conversations:\n\n- **Agent State** - What the LLM thinks with: complete message history, todos, and middleware state stored as a single serialized blob\n- **Display Messages** - What users see: individual UI-friendly records optimized for rendering, streaming, and rich content types\n\nThis separation enables:\n- **State optimization**: Agent conversation history can be summarized/compacted to reduce token usage without affecting what users see\n- **Efficient UI queries**: Load display messages without deserializing agent state\n- **Progressive streaming**: Real-time updates as messages arrive via PubSub\n- **Flexible rendering**: Show thinking blocks, tool status, images - content the LLM doesn't need\n\nBoth views link through the `conversations` table, which you connect to your application's owner model (users, teams, etc.).\n\nSee [Conversations Architecture](docs/conversations_architecture.md) for the complete explanation with diagrams.\n\n## Documentation\n\n- [Conversations Architecture](docs/conversations_architecture.md) - How the dual-view pattern works with agent state and display messages\n- [Lifecycle Management](docs/lifecycle.md) - Process supervision, timeouts, and shutdown\n- [Subscriptions \u0026 Presence](docs/subscriptions_and_presence.md) - Real-time events, agent-discovery presence, and viewer-presence shutdown\n- [Middleware Development](docs/middleware.md) - Building custom middleware\n- [Observability](docs/observability.md) - Middleware-based callbacks for OpenTelemetry, metrics, and logging\n- [State Persistence](docs/persistence.md) - Saving and restoring conversations\n- [Middleware Messaging](docs/middleware_messaging.md) - Async messaging between middleware and AgentServer\n- [Architecture Overview](docs/architecture.md) - System design and data flow\n\n## Development\n\n```bash\n# Install dependencies\nmix deps.get\n\n# Run tests\nmix test\n\n# Run tests with live API calls (requires API keys, incurs costs)\nmix test --include live_call\nmix test --include live_anthropic\n\n# Pre-commit checks\nmix precommit\n```\n\n## Acknowledgments\n\nSagents was originally inspired by the [LangChain Deep Agents](https://langchain-ai.github.io/langgraph/agents/overview/) project, though it has evolved into its own comprehensive framework tailored for Elixir and Phoenix applications.\n\nBuilt on top of [Elixir LangChain](https://github.com/brainlid/langchain), which provides the core LLM integration layer.\n\n## License\n\nApache-2.0 license - see [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsagents-ai%2Fsagents","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsagents-ai%2Fsagents","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsagents-ai%2Fsagents/lists"}