Packages

An Elixir and OTP framework for building AI agents.

Current section

Files

Jump to
agent_ex design.md
Raw

design.md

# AgentEx Product Requirements Document
## 1. Product overview
### Product name
**AgentEx**
### Product type
Open-source Elixir framework for building, running, supervising, coordinating, and persisting AI agents.
### Summary
AgentEx provides a native Elixir and OTP runtime for building AI agents without forcing developers to compose many unrelated abstractions.
The framework should make simple agents simple while still supporting:
* tool calling;
* persistent conversations;
* many concurrent agent threads;
* human approvals;
* model-provider adapters;
* MCP tools;
* shared-chat multi-agent teams;
* stateful workflows;
* parallel workflow branches;
* checkpointing;
* streaming;
* telemetry;
* bulk execution through fleets.
AgentEx should feel like an Elixir-native agent runtime rather than a wrapper around a Python framework.
Its core design should map directly to OTP concepts:
```text
Agent definition → Elixir module
Agent thread → supervised process
Thread lookup → Registry
Thread lifecycle → DynamicSupervisor
Parallel execution → supervised Tasks
Persistence → configurable memory backend
```
---
# 2. Problem statement
Existing agent frameworks often expose too many overlapping concepts:
```text
agent
runner
executor
session
chain
graph
runtime
workflow
memory
context
state
orchestrator
```
This creates several problems:
1. Developers must understand the framework’s internal architecture before running a basic agent.
2. Stateful and stateless execution often use completely different APIs.
3. Multi-agent systems frequently rely on ad hoc routing and isolated histories.
4. Graph and workflow systems expose low-level graph terminology even when users are describing business workflows.
5. Elixir developers cannot easily take advantage of OTP supervision, lightweight processes, fault isolation, and registries.
6. Model providers, tools, persistence, tracing, and approvals are often tightly coupled.
7. Scaling from one agent to thousands of concurrent agent sessions requires custom infrastructure.
AgentEx should solve this by offering a small set of consistent concepts.
---
# 3. Product vision
AgentEx should become the standard Elixir runtime for building applications such as:
* AI assistants;
* coding agents similar to Claude Code;
* personal-agent platforms similar to OpenClaw;
* customer-support agents;
* research agents;
* background monitoring agents;
* document-processing systems;
* multi-agent collaboration systems;
* approval-driven automation;
* stateful workflow applications;
* large multi-tenant agent platforms.
The framework should make this progression natural:
```text
One agent
→ persistent agent threads
→ multiple collaborating agents
→ explicit workflows
→ thousands of concurrent jobs
```
---
# 4. Product principles
## 4.1 Simplicity first
A basic agent should require only:
```elixir
defmodule ResearchAgent do
use AgentEx
model "openai:gpt-5"
system "You are a research assistant."
tools [
SearchWeb
]
end
```
And should run through:
```elixir
ResearchAgent.run("Research Phoenix.")
```
## 4.2 OTP-native
AgentEx must reuse OTP rather than recreate it.
It should use:
* `Supervisor`;
* `DynamicSupervisor`;
* `Registry`;
* `GenServer`;
* `Task.Supervisor`;
* process monitoring;
* standard process messaging;
* `:telemetry`;
* ETS where appropriate.
AgentEx must not introduce custom replacements for existing OTP primitives.
## 4.3 One consistent API
The same high-level API should apply across agents, teams, and workflows:
```elixir
run(...)
prompt(...)
save(...)
stop(...)
active_threads(...)
alive_threads(...)
approve(...)
reject(...)
edit(...)
respond(...)
```
## 4.4 Explicit state ownership
Each runtime abstraction must have one clear source of truth.
* Agent thread: thread conversation state.
* Team thread: shared team-chat history.
* Workflow thread: workflow state.
* Fleet: job and scheduling state.
## 4.5 Extensible, not coupled
AgentEx core must not be tightly coupled to:
* OpenAI;
* Anthropic;
* Neo4j;
* Postgres;
* a specific vector store;
* a specific telemetry UI;
* a specific MCP implementation.
All external integrations should use behaviours and adapters.
## 4.6 Safe defaults
Potentially dangerous actions should support approval requirements.
Concurrency, loops, retries, costs, and tool calls should have configurable limits.
## 4.7 Stateless by default, stateful by choice
`run/2` should be ephemeral by default.
`prompt/3` should be used when the caller explicitly wants a persistent thread.
---
# 5. Core terminology
## 5.1 Agent
An agent is a reusable behavior definition.
It contains:
* a model;
* a system prompt;
* tools;
* callbacks;
* limits;
* memory configuration;
* persistence configuration.
An agent is not a process and is not a single conversation.
The Elixir module is the agent definition.
```elixir
ResearchAgent
SecurityAgent
WriterAgent
```
## 5.2 Thread
A thread is one stateful instance of an agent, team, or workflow.
It contains:
* an ID;
* state;
* lifecycle metadata;
* pending approvals;
* checkpoints;
* runtime status.
A thread is represented by a supervised process while alive.
## 5.3 Team
A team is a reusable collection of agent participants operating in one shared chat.
The shared team-chat history is the source of truth.
Participants do not maintain separate hidden conversation histories by default.
## 5.4 Workflow
A workflow is an explicit stateful execution structure containing:
* workflow state;
* steps;
* transitions;
* named parallel splits;
* joins;
* conditional routing;
* cycles;
* interrupts;
* checkpoints.
The public abstraction is called `Workflow`, although it may internally compile into a graph.
## 5.5 Fleet
A Fleet coordinates large numbers of agent, team, or workflow jobs.
It provides:
* bounded concurrency;
* queueing;
* retries;
* status;
* pause and resume;
* cancellation;
* result collection;
* result streaming.
## 5.6 Tool
A tool is a capability exposed to a model.
Tools may be:
* Elixir modules;
* Elixir functions;
* anonymous functions;
* MCP tools.
## 5.7 Provider
A provider connects AgentEx to a model backend.
Examples:
* OpenAI;
* Anthropic;
* Google;
* OpenRouter;
* local OpenAI-compatible servers;
* custom providers.
---
# 6. Target users
## Primary users
* Elixir and Phoenix developers;
* teams building AI-powered SaaS products;
* developers building multi-agent systems;
* developers building persistent assistants;
* infrastructure engineers building high-concurrency agent systems;
* companies that want AI agents inside existing Elixir applications.
## Secondary users
* researchers experimenting with agent architectures;
* developers building coding agents;
* developers building self-hosted personal agents;
* teams migrating agent systems away from Python.
---
# 7. Main use cases
## 7.1 Basic agent
Run one model with tools.
```elixir
ResearchAgent.run("Research Phoenix.")
```
## 7.2 Stateful assistant
Maintain a persistent conversation.
```elixir
ResearchAgent.prompt(
{:new, "phoenix"},
"Research Phoenix."
)
ResearchAgent.prompt(
"phoenix",
"Who maintains it?"
)
```
## 7.3 Approval-driven automation
Allow the model to prepare an action but require human approval before execution.
```elixir
tool SendEmail,
approval: :required
```
## 7.4 Agentic RAG
Expose document retrieval as a normal tool.
```elixir
tools [
SearchDocuments
]
```
## 7.5 Shared-chat multi-agent collaboration
Allow researcher, security, and writer agents to collaborate in one canonical conversation.
## 7.6 Explicit workflow
Run agents, teams, tools, functions, and sub-workflows according to explicit transitions.
## 7.7 Large-scale execution
Run thousands of independent agent threads with bounded provider concurrency.
## 7.8 MCP integration
Import tools exposed by external MCP servers and use them as normal AgentEx tools.
## 7.9 Coding-agent platform
Build a Claude Code-style product using:
* filesystem tools;
* shell tools;
* code search;
* patching;
* testing;
* approvals;
* persistent threads;
* terminal UI.
## 7.10 Personal-agent platform
Build an OpenClaw-style product using:
* persistent agents;
* scheduled jobs;
* WhatsApp, Telegram, or Slack integrations;
* AgentEx tools;
* MCP;
* memory;
* a Phoenix gateway;
* a dashboard.
---
# 8. Agent definition API
## 8.1 Basic definition
```elixir
defmodule ResearchAgent do
use AgentEx,
memory: :ets,
persistence:
{:checkpoint,
every: :timer.minutes(5),
changes: 10,
idle: :timer.minutes(1),
on: [:approval, :stop]}
model "openai:gpt-5"
system """
You are an open-source research assistant.
"""
tools [
SearchGitHub,
RepoStats
]
end
```
## 8.2 Agent configuration
Agent definitions should support:
```elixir
model "provider:model-name"
system "..."
tools [...]
memory :memory
memory :ets
memory MyCustomMemory
persistence :none
persistence :always
persistence {:checkpoint, ...}
max_steps 20
max_tool_calls 10
timeout :timer.minutes(2)
max_cost_usd 0.50
before_run &callback/1
after_run &callback/1
before_model &callback/1
after_model &callback/1
before_tool &callback/1
after_tool &callback/1
```
---
# 9. Model providers
## 9.1 Provider namespace
Provider-related modules must use the `AgentEx.Provider` prefix.
```text
AgentEx.Provider
AgentEx.Provider.Registry
AgentEx.Provider.Response
AgentEx.Provider.Stream
AgentEx.Provider.Local
```
Optional provider packages expose:
```text
AgentEx.Provider.OpenAI
AgentEx.Provider.Anthropic
AgentEx.Provider.Google
AgentEx.Provider.OpenRouter
```
## 9.2 Provider resolution
```elixir
model "openai:gpt-5"
```
is parsed into:
```elixir
%AgentEx.Model{
provider: AgentEx.Provider.OpenAI,
name: "gpt-5",
options: []
}
```
Provider resolution order:
```text
Explicit provider module
→ application provider registry
→ built-in provider registry
→ unknown-provider error
```
## 9.3 Separate provider packages
AgentEx core should remain small.
Suggested packages:
```text
agent_ex
agent_ex_openai
agent_ex_anthropic
agent_ex_google
agent_ex_openrouter
```
## 9.4 Built-in local provider
Core should include a local OpenAI-compatible provider.
```elixir
model "local:qwen3",
base_url: "http://localhost:11434/v1"
```
It should support services such as:
* Ollama;
* llama.cpp servers;
* vLLM;
* LocalAI;
* other OpenAI-compatible endpoints.
## 9.5 Custom providers
```elixir
defmodule MyApp.CustomProvider do
@behaviour AgentEx.Provider
@impl true
def call(model, messages, tools, opts) do
...
end
end
```
Direct usage:
```elixir
model MyApp.CustomProvider,
name: "reasoner-v2"
```
Registered usage:
```elixir
config :agent_ex, :providers,
acme: MyApp.CustomProvider
```
```elixir
model "acme:reasoner-v2"
```
## 9.6 Provider responsibilities
A provider must:
1. Convert AgentEx messages to provider format.
2. Convert AgentEx tool specifications to provider format.
3. Call the provider API.
4. Normalize text responses.
5. Normalize tool calls.
6. Normalize token and cost usage.
7. Support streaming where available.
8. Return typed errors.
---
# 10. Tool API
## 10.1 Module tools
```elixir
defmodule SearchGitHub do
use AgentEx.Tool
name :search_github
description "Search GitHub repositories."
input do
field :query, :string, required: true
field :limit, :integer, default: 10
end
def call(%{query: query, limit: limit}, context) do
GitHub.search(query,
limit: limit,
user_id: context.user_id
)
end
end
```
The `input` declaration is optional. When it is omitted, AgentEx should use an
empty object schema and call the tool with `%{}`.
```elixir
defmodule CurrentTime do
use AgentEx.Tool
description "Get the current UTC date and time."
def call(%{}, _context) do
{:ok, DateTime.utc_now()}
end
end
```
Tool names may be atoms or strings.
They must be normalized internally to strings.
If a module does not explicitly declare a name, AgentEx should derive one from the module name.
```text
RepoStats → repo_stats
```
## 10.2 Function tools
```elixir
tool :search_docs,
description: "Search internal documents.",
input: [
query: [type: :string, required: true]
],
call: &search_docs/2
```
Anonymous function:
```elixir
tool :current_time,
description: "Return the current application time.",
input: [],
call: fn _args, _context ->
DateTime.utc_now()
end
```
## 10.3 Multiple tool declarations
```elixir
tools [
SearchGitHub,
{RepoStats,
name: :repository_stats},
{SendMessage,
approval: :required},
{:get_maintainers,
description: "Get repository maintainers.",
input: [
repository: [type: :string, required: true]
],
call: &get_maintainers/2}
]
```
Supported forms:
```elixir
ToolModule
{ToolModule, opts}
{:function_tool_name, opts}
{:mcp, server_name, opts}
```
Duplicate normalized names must cause a compile-time error where possible.
## 10.4 Tool callbacks
Tool module callbacks:
```elixir
before_call &validate/1
after_call &audit/1
```
Registration callbacks:
```elixir
tool SendMessage,
before_call: &check_permissions/1,
after_call: &record_delivery/1
```
Agent-level callbacks:
```elixir
before_tool &log_tool/1
after_tool &record_tool_result/1
```
Callbacks should return either:
```elixir
context
```
or:
```elixir
{:halt, reason}
```
## 10.5 Tool execution options
```elixir
tool SearchGitHub,
timeout: 15_000,
retries: 2,
backoff: :exponential
```
## 10.6 Tool normalization
All tools should become:
```elixir
%AgentEx.Tool.Spec{
name: "search_github",
description: "...",
input_schema: %{...},
executor: ...
}
```
The main agent loop must not need to know whether the executor is:
* a module;
* a function;
* an MCP call.
---
# 11. MCP support
## 11.1 Purpose
AgentEx should act as an MCP client so external MCP tools can be used like normal AgentEx tools.
MCP should support:
* tools;
* resources;
* prompts;
* stdio transport;
* HTTP transport.
## 11.2 Inline MCP server
```elixir
mcp :github,
command: "github-mcp-server",
args: ["stdio"]
```
## 11.3 Remote MCP server
```elixir
mcp :internal,
url: "https://mcp.example.com",
headers: fn context ->
[
{"authorization", "Bearer #{context.access_token}"}
]
end
```
## 11.4 MCP tool declaration
```elixir
tool :search_repositories,
mcp: :github
```
Rename locally:
```elixir
tool :github_search,
mcp: :github,
remote: :search_repositories
```
Normal tool options remain available:
```elixir
tool :create_issue,
mcp: :github,
approval: :required,
timeout: 30_000,
retries: 2
```
## 11.5 Import MCP tools
Import all:
```elixir
tools [
{:mcp, :github}
]
```
Import selected:
```elixir
tools [
{:mcp, :github,
only: [
:search_repositories,
:get_issue,
:create_issue
]}
]
```
Exclude tools:
```elixir
tools [
{:mcp, :github,
except: [:delete_repository]}
]
```
## 11.6 Reusable MCP module
```elixir
defmodule MyApp.GitHubMCP do
use AgentEx.MCP
transport :stdio,
command: "github-mcp-server",
args: ["stdio"]
end
```
Usage:
```elixir
tools [
{MyApp.GitHubMCP,
only: [:search_repositories, :get_issue]}
]
```
## 11.7 MCP tool execution
MCP tools should normalize into ordinary tool specs:
```elixir
%AgentEx.Tool.Spec{
name: "search_repositories",
description: "...",
input_schema: %{...},
executor:
{AgentEx.MCP.Tool,
server: :github,
remote_name: "search_repositories"}
}
```
## 11.8 Future MCP server support
A later version may allow AgentEx to expose:
* tools;
* agents;
* teams;
* workflows;
as an MCP server.
This is not required for the first release.
---
# 12. Messages
## 12.1 Message structure
```elixir
%AgentEx.Message{
role: :user,
content: "Hello",
metadata: %{}
}
```
Supported roles:
```text
:system
:user
:assistant
:tool
```
## 12.2 Constructors
```elixir
AgentEx.Message.system(content)
AgentEx.Message.user(content)
AgentEx.Message.assistant(content)
AgentEx.Message.tool(content, opts)
```
## 12.3 Multiple-message input
```elixir
ResearchAgent.run([
system: "Be concise.",
user: "Tell me about Phoenix.",
assistant: "Phoenix is an Elixir framework.",
user: "Who created it?"
])
```
The shorthand list must normalize into `%AgentEx.Message{}` values.
## 12.4 Metadata
Messages may support metadata such as:
* sender;
* participant;
* tool call ID;
* provider ID;
* timestamps;
* attachments;
* tracing attributes.
Metadata should be optional and must not complicate simple usage.
---
# 13. Agent execution model
## 13.1 One-off execution
```elixir
ResearchAgent.run("What is Phoenix?")
```
Default behavior:
```text
Create temporary execution state
→ run model/tool loop
→ return response
→ terminate execution
```
Properties:
* no persistent thread process;
* no generated thread ID;
* `thread_id` is `nil`;
* no state is persisted by default;
* does not accept a PID;
* `save` defaults to `false`.
Response:
```elixir
%AgentEx.Response{
content: "Phoenix is...",
message: %AgentEx.Message{},
thread_id: nil,
tool_calls: [],
usage: %{}
}
```
## 13.2 Run against stored state
```elixir
ResearchAgent.run(
"Summarize the thread.",
thread: {:existing, "phoenix"},
save: false
)
```
Persist the resulting state:
```elixir
ResearchAgent.run(
"Check for updates.",
thread: {:existing, "phoenix"},
save: true
)
```
This should load state, execute ephemerally, optionally save, and exit without leaving a live thread worker.
## 13.3 Stateful prompt
Generated thread ID:
```elixir
ResearchAgent.prompt(
:new,
"Research Phoenix."
)
```
Supplied ID:
```elixir
ResearchAgent.prompt(
{:new, "phoenix"},
"Research Phoenix."
)
```
Continue:
```elixir
ResearchAgent.prompt(
"phoenix",
"Who maintains it?"
)
```
By PID:
```elixir
ResearchAgent.prompt(
pid,
"Continue."
)
```
## 13.4 Thread collision behavior
Creating a supplied thread ID that already exists must return:
```elixir
{:error, :thread_already_exists}
```
Continuing an unknown thread must return:
```elixir
{:error, :thread_not_found}
```
## 13.5 Thread ID generator
Agents should support a custom thread ID generator.
```elixir
use AgentEx,
thread_id_generator: &MyApp.ThreadIDs.generate/1
```
The generator should receive enough context to generate stable IDs, including:
* agent module;
* creation options;
* optional caller context.
---
# 14. Agent loop
The core agent loop should behave as follows:
```text
Input messages
→ call provider
→ receive assistant response or tool calls
→ execute tools
→ append tool results
→ call provider again
→ repeat
→ final assistant response
```
The loop must support:
* multiple tool calls;
* sequential tool rounds;
* streaming;
* approvals;
* limits;
* callbacks;
* usage collection;
* tracing;
* errors;
* cancellation.
---
# 15. Thread architecture
## 15.1 Per-agent supervision tree
Adding an agent to an application supervisor:
```elixir
children = [
ResearchAgent
]
```
should start the agent’s internal runtime infrastructure.
Conceptually:
```text
ResearchAgent.Supervisor
├── Registry
├── DynamicSupervisor
└── CheckpointManager
```
`ResearchAgent.start_link/1` starts the infrastructure, not a conversation thread.
Threads are started through `prompt/3`.
## 15.2 Thread registration
Each live thread should be registered using:
```elixir
{ResearchAgent, thread_id}
```
## 15.3 Thread lookup behavior
```text
Thread is alive
→ route request to existing process
Thread exists but is not alive
→ load state
→ start process
→ execute request
Thread does not exist
→ return error
```
## 15.4 Prompt behavior while running
Default behavior:
```text
idle thread → start a new run
running thread → queue prompt
interrupted thread→ await approval or resume input
```
A later version may allow configurable policies:
```text
:queue
:reject
:interrupt
```
---
# 16. Thread lifecycle
## 16.1 Active
`active` means:
> This thread is intended to remain available and be restored after an application restart.
It is a persisted desired state.
## 16.2 Alive
`alive` means:
> This thread currently has a running process.
It is derived from the runtime Registry and is never persisted.
## 16.3 Example restart lifecycle
Before restart:
```text
active: true
alive: true
```
Immediately after restart:
```text
active: true
alive: false
```
After restoration:
```text
active: true
alive: true
```
## 16.4 In-memory backend restart behavior
With:
```elixir
memory: :memory
```
all thread state and lifecycle metadata are lost after a full application restart.
No thread can be restored.
## 16.5 Stop behavior
```elixir
ResearchAgent.stop("phoenix")
```
must:
1. Persist `active: false`.
2. Save or checkpoint state.
3. Terminate the worker.
Persisting inactive status must occur before process termination.
## 16.6 Listing
```elixir
ResearchAgent.active_threads()
ResearchAgent.alive_threads()
ResearchAgent.thread("phoenix")
```
---
# 17. Memory
## 17.1 Process memory
```elixir
memory: :memory
```
Characteristics:
* state lives in the thread process;
* fastest simple option;
* lost when process/application state is lost;
* no restoration after a full restart.
## 17.2 ETS memory
```elixir
memory: :ets
```
Characteristics:
* shared in-memory storage;
* fast concurrent access;
* survives individual thread process restarts;
* lost after a complete BEAM restart unless paired with an external persistence backend.
## 17.3 Custom backend
```elixir
memory: MyApp.AgentMemory
```
Suggested behaviour:
```elixir
@callback load(agent, thread_id, opts) ::
{:ok, state}
| :not_found
| {:error, term()}
@callback save(agent, thread_id, state, opts) ::
:ok
| {:error, term()}
@callback set_active(agent, thread_id, boolean, opts) ::
:ok
| {:error, term()}
@callback active_threads(agent, opts) ::
{:ok, [thread_id]}
| {:error, term()}
```
## 17.4 Memory modes
The system should clearly separate:
```text
Working memory
= current state used by the live process
Persistence backend
= durable external state
Checkpoint policy
= when working memory is flushed
```
---
# 18. Persistence
## 18.1 None
```elixir
persistence: :none
```
No automatic persistence of conversation state.
Manual save remains possible where a backend supports it.
## 18.2 Always
```elixir
persistence: :always
```
Persist every state change.
## 18.3 Checkpoint
```elixir
persistence:
{:checkpoint,
every: :timer.minutes(5),
changes: 10,
idle: :timer.minutes(1),
on: [:approval, :stop]}
```
A checkpoint occurs when any configured trigger matches.
Supported triggers:
* time interval;
* state-change count;
* idle duration;
* specific events.
Potential events:
```text
:approval
:interrupt
:tool_call
:stop
:end
```
## 18.4 Lifecycle metadata
Lifecycle metadata must persist immediately regardless of conversation-state policy.
Examples:
```text
active
inactive
created_at
updated_at
```
Conversation state follows the configured persistence policy.
Examples:
```text
messages
tool history
agent state
workflow state
team history
```
## 18.5 Manual saving
```elixir
ResearchAgent.save()
ResearchAgent.save("phoenix")
ResearchAgent.save(pid)
```
`save/0` saves all dirty threads for the agent.
---
# 19. Approvals
## 19.1 Tool declaration
```elixir
tool SendMessage,
approval: :required
```
## 19.2 Approval result
When the model requests the tool:
```elixir
{:approval_required, approval}
```
## 19.3 Approval structure
```elixir
%AgentEx.Approval{
id: "approval-123",
agent: ResearchAgent,
thread_id: "phoenix",
tool: "send_message",
arguments: %{}
}
```
## 19.4 Supported decisions
Approve:
```elixir
ResearchAgent.approve(approval.id)
```
Reject:
```elixir
ResearchAgent.reject(
approval.id,
reason: "Do not send it."
)
```
Edit and execute:
```elixir
ResearchAgent.edit(
approval.id,
arguments: updated_arguments
)
```
Respond to model:
```elixir
ResearchAgent.respond(
approval.id,
"Rewrite the message."
)
```
## 19.5 Allowed decisions
Tools may restrict available approval decisions.
```elixir
tool SendMessage,
approval: [
allowed_decisions: [
:approve,
:edit,
:reject,
:respond
]
]
```
## 19.6 Persistence
An approval interruption must checkpoint enough state to resume the exact run after:
* process restart;
* application restart;
* delayed user decision.
---
# 20. Streaming and events
## 20.1 Agent streaming
```elixir
ResearchAgent.prompt(
"phoenix",
"Continue.",
stream: fn
{:token, token} ->
IO.write(token)
{:tool_started, call} ->
IO.inspect(call)
{:tool_finished, result} ->
IO.inspect(result)
{:approval_required, approval} ->
IO.inspect(approval)
{:completed, response} ->
IO.inspect(response)
end
)
```
## 20.2 Event model
AgentEx should expose normalized events such as:
```text
run_started
provider_started
token
tool_started
tool_finished
approval_required
thread_started
checkpoint_saved
team_message
workflow_step_started
workflow_split_started
workflow_join_completed
run_completed
run_failed
```
## 20.3 Event consumers
Events should be usable by:
* LiveView;
* terminal interfaces;
* logs;
* telemetry;
* OpenTelemetry;
* dashboards;
* audit systems.
---
# 21. Teams
## 21.1 Team mental model
A team is one shared chat with many participants.
```text
Team thread
├── user
├── researcher
├── security
└── writer
```
The canonical shared history is the source of truth.
Participants do not keep separate hidden chat histories by default.
## 21.2 Participant ownership
Each participant owns only its configuration:
* model;
* system prompt;
* tools;
* permissions;
* participant-specific glue.
## 21.3 Team message
```elixir
%AgentEx.Message{
role: :assistant,
sender: :security,
content: "I found one dependency concern."
}
```
## 21.4 Team modes
Teams support:
```text
:coordinated
:broadcast
```
---
# 22. Coordinated teams
## 22.1 Definition
```elixir
defmodule ReviewTeam do
use AgentEx.Team
participants [
researcher: ResearchAgent,
security: SecurityAgent,
writer: WriterAgent
]
coordinator :researcher
end
```
## 22.2 Flow
```text
User message
→ coordinator
→ coordinator delegates work
→ participants post updates
→ coordinator decides when finished
→ final response
```
## 22.3 Delegation results
Coordinator actions may include:
```elixir
{:delegate,
[
{:researcher, "Check repository activity."},
{:security, "Check known advisories."}
]}
```
Parallel delegation:
```elixir
{:parallel,
[
{:researcher, "Inspect maintenance activity."},
{:security, "Inspect vulnerabilities."}
]}
```
Finish:
```elixir
{:done, "Final answer"}
```
---
# 23. Broadcast teams
## 23.1 Definition
```elixir
defmodule ReviewTeam do
use AgentEx.Team,
memory: :ets
participants [
researcher: ResearchAgent,
security: SecurityAgent,
writer: WriterAgent
]
mode :broadcast
glue """
A new message was added to the shared chat.
Sender: {{sender}}
Message: {{message}}
Read the full shared history.
Interpret this event from your role.
Respond when useful.
Otherwise return :pass.
"""
max_rounds 5
end
```
## 23.2 Glue prompt
The glue prompt is the adapter between a committed shared-chat message and a participant invocation.
It should provide:
* sender;
* new message;
* team identity;
* participant identity;
* full shared history;
* instructions for interpreting the update.
Effective participant invocation:
```text
Participant system prompt
+ team glue prompt
+ participant glue override
+ canonical shared history
+ new-message event
```
## 23.3 Broadcast flow
```text
New message committed
→ render glue prompt
→ invoke eligible participants
→ participant responds or passes
→ responses are committed
→ new responses become events
→ repeat
```
## 23.4 Participant results
```elixir
{:message, "I found two risks."}
:pass
:done
```
Targeted result:
```elixir
{:message,
"Verify this dependency.",
audience: [:security]}
```
## 23.5 Loop prevention
Broadcast teams must stop when:
* every eligible participant returns `:pass`;
* a participant returns `:done`;
* `max_rounds` is reached;
* an approval is required;
* an interrupt occurs;
* the user stops the thread.
## 23.6 Participant options
```elixir
participant :security, SecurityAgent,
receive_from: [:user, :researcher],
glue: """
Interpret updates for security implications.
""",
max_responses: 3
```
Array form:
```elixir
participants [
{:researcher, ResearchAgent},
{:security, SecurityAgent,
receive_from: [:user, :researcher]},
{:writer, WriterAgent,
receive_from: :all}
]
```
## 23.7 Team API
```elixir
ReviewTeam.prompt(thread, input, opts \\ [])
ReviewTeam.save()
ReviewTeam.save(thread)
ReviewTeam.stop(thread)
ReviewTeam.thread(thread_id)
ReviewTeam.active_threads()
ReviewTeam.alive_threads()
ReviewTeam.approve(approval_id, opts \\ [])
ReviewTeam.reject(approval_id, opts \\ [])
ReviewTeam.edit(approval_id, opts)
ReviewTeam.respond(approval_id, message)
```
---
# 24. Workflows
## 24.1 Purpose
Workflows provide explicit, stateful execution similar in capability to LangGraph while using AgentEx terminology.
The public vocabulary is:
```text
workflow
state
step
transition
split
join
```
## 24.2 Workflow state
The workflow owns its state.
```elixir
state do
field :repository, :string, required: true
field :research, :string
field :security, :string
field :critical?, :boolean, default: false
field :findings, {:array, :map},
default: [],
reducer: &Kernel.++/2
end
```
Steps receive state and return partial state updates.
## 24.3 Full example
```elixir
defmodule ReviewWorkflow do
use AgentEx.Workflow,
memory: :ets,
persistence:
{:checkpoint,
on: [:interrupt, :approval, :stop, :end]}
state do
field :repository, :string, required: true
field :research, :string
field :security, :string
field :quality, :string
field :critical?, :boolean, default: false
field :report, :string
field :findings, {:array, :map},
default: [],
reducer: &Kernel.++/2
end
step :research, &research/1
step :security,
agent: SecurityAgent,
input: &security_prompt/1,
output: &store_security/2
step :quality,
agent: QualityAgent,
input: &quality_prompt/1,
output: :quality
step :escalate,
tool: NotifySecurityTeam,
input: &escalation_input/1
step :report,
team: ReportTeam,
input: &report_prompt/1,
output: :report
start :research
split :review_checks,
from: :research,
to: [:security, :quality]
join :review_checks,
to: :route_review,
wait: :all
step :route_review, &route_review/1
transition :route_review,
when: &critical?/1,
to: :escalate
transition :route_review,
otherwise: :report
transition :escalate, to: :report
transition :report, to: :end
end
```
---
# 25. Workflow step types
## 25.1 Function step
```elixir
step :prepare, &prepare/1
```
```elixir
def prepare(state) do
%{repository: String.trim(state.repository)}
end
```
## 25.2 Inline step
```elixir
step :prepare, fn state ->
%{repository: String.trim(state.repository)}
end
```
## 25.3 Agent step
```elixir
step :research,
agent: ResearchAgent,
input: &research_prompt/1,
output: :research
```
Agent steps should be stateless by default.
Internally they call:
```elixir
ResearchAgent.run(prompt)
```
The workflow remains the state source of truth.
## 25.4 Stateful agent step
```elixir
step :research,
agent: ResearchAgent,
thread: fn state ->
"research-#{state.repository}"
end,
input: &research_prompt/1,
output: :research
```
This explicitly introduces a second state layer and should be opt-in.
## 25.5 Team step
```elixir
step :review,
team: ReviewTeam,
input: &review_prompt/1,
output: :review
```
## 25.6 Tool step
```elixir
step :notify,
tool: SendMessage,
input: &notification_input/1
```
## 25.7 Sub-workflow step
```elixir
step :audit,
workflow: AuditWorkflow,
input: &audit_input/1,
output: :audit
```
---
# 26. Workflow input and output mapping
## 26.1 Input mapping
```elixir
step :security,
agent: SecurityAgent,
input: fn state ->
"""
Repository: #{state.repository}
Research:
#{state.research}
"""
end
```
Without an input mapper, the full workflow state may be passed.
## 26.2 Output key
```elixir
output: :security
```
This stores the normalized response under `state.security`.
## 26.3 Output function
```elixir
output: fn response, state ->
%{
security: response.content,
critical?: response.metadata[:critical?] || false
}
end
```
## 26.4 Ignore output
```elixir
output: :ignore
```
---
# 27. Workflow transitions
## 27.1 Start
```elixir
start :research
```
## 27.2 Normal transition
```elixir
transition :research, to: :security
transition :report, to: :end
```
## 27.3 Conditional transition
```elixir
transition :security,
when: &critical?/1,
to: :escalate
transition :security,
otherwise: :report
```
Conditions are evaluated in declaration order.
## 27.4 Dynamic transition
```elixir
transition :classify,
to: &route/1
```
```elixir
def route(state) do
cond do
state.critical? -> :escalate
state.needs_review? -> :review
true -> :report
end
end
```
## 27.5 Cycles
```elixir
transition :write, to: :review
transition :review,
when: &needs_revision?/1,
to: :write
transition :review,
otherwise: :end
```
Cycle protection:
```elixir
max_steps 50
max_visits :write, 5
```
---
# 28. Named parallel splits and joins
## 28.1 Named split
```elixir
split :review_checks,
from: :research,
to: [:security, :quality]
```
The split name prevents the caller from having to repeat branch lists manually.
## 28.2 Join
```elixir
join :review_checks,
to: :combine,
wait: :all
```
Supported join policies:
```elixir
wait: :all
wait: :any
wait: {:count, 2}
```
Default:
```elixir
wait: :all
```
## 28.3 Conditional split
```elixir
split :required_checks,
from: :classify,
to: &required_checks/1
```
The runtime must store the branches selected for that specific run.
The matching join waits only for selected branches.
## 28.4 Parallel state snapshots
Every branch receives the same state snapshot taken at split time.
## 28.5 State merging
Different keys merge normally.
```elixir
%{security: result}
%{quality: result}
```
Concurrent writes to the same key require a reducer.
```elixir
field :findings, {:array, :map},
default: [],
reducer: &Kernel.++/2
```
Without a reducer:
```elixir
{:error, {:state_conflict, :findings}}
```
## 28.6 Runtime split state
Conceptually:
```elixir
%AgentEx.Workflow.Split{
name: :review_checks,
selected_branches: [:security, :quality],
completed: MapSet.new(),
results: %{}
}
```
---
# 29. Workflow interrupts
## 29.1 Create interrupt
```elixir
def review(state) do
AgentEx.Workflow.interrupt(
:human_review,
%{report: state.report}
)
end
```
Result:
```elixir
{:interrupted, interrupt}
```
## 29.2 Resume
```elixir
ReviewWorkflow.resume(
interrupt.id,
%{approved: true}
)
```
## 29.3 Checkpoint requirements
A workflow checkpoint must include:
* workflow state;
* current step;
* completed steps;
* active splits;
* selected branches;
* completed branches;
* pending joins;
* approvals;
* interrupts;
* execution limits;
* relevant usage metadata.
---
# 30. Workflow errors
## 30.1 Per-step retry
```elixir
step :security,
agent: SecurityAgent,
retries: 3,
backoff: :exponential,
timeout: 30_000
```
## 30.2 Error transition
```elixir
transition :security,
on_error: :security_failed
```
## 30.3 Global error handler
```elixir
on_error &handle_error/2
```
Possible return values:
```elixir
{:retry, opts}
{:continue, next_step, state_update}
{:halt, reason}
```
---
# 31. Workflow API
```elixir
Workflow.run(input, opts \\ [])
Workflow.prompt(
thread,
input,
opts \\ []
)
Workflow.resume(
interrupt_id,
input,
opts \\ []
)
Workflow.save()
Workflow.save(thread)
Workflow.stop(thread)
Workflow.thread(thread_id)
Workflow.active_threads()
Workflow.alive_threads()
Workflow.approve(approval_id, opts \\ [])
Workflow.reject(approval_id, opts \\ [])
Workflow.edit(approval_id, opts)
Workflow.respond(approval_id, message)
```
---
# 32. Fleet
## 32.1 Purpose
Fleet coordinates many independent agent, team, or workflow executions.
It is not another type of agent.
It is an operational scheduler.
## 32.2 Definition
```elixir
defmodule ResearchFleet do
use AgentEx.Fleet,
agent: ResearchAgent,
max_running: 100,
max_queue: 10_000
end
```
Potential targets:
```elixir
agent: ResearchAgent
team: ReviewTeam
workflow: ReviewWorkflow
```
## 32.3 Jobs
```elixir
jobs =
for id <- 1..4_000 do
%{
thread: {:new, "thread-#{id}"},
input: "Research project #{id}"
}
end
```
## 32.4 Start
```elixir
{:ok, run} =
ResearchFleet.start(jobs)
```
## 32.5 Run state
```elixir
%AgentEx.Fleet.Run{
id: "fleet-run-123",
total: 4_000,
queued: 3_900,
running: 100,
completed: 0,
failed: 0
}
```
## 32.6 Control API
```elixir
ResearchFleet.status(run.id)
ResearchFleet.results(run.id)
ResearchFleet.stream(run.id)
ResearchFleet.pause(run.id)
ResearchFleet.resume(run.id)
ResearchFleet.cancel(run.id)
ResearchFleet.cancel(run.id, force: true)
ResearchFleet.retry_failed(
run.id,
max_attempts: 3
)
ResearchFleet.save()
ResearchFleet.save(run.id)
```
## 32.7 Concurrency
The framework must distinguish:
```text
4,000 alive threads
4,000 simultaneous model calls
```
Fleet should enforce bounded provider and tool concurrency.
## 32.8 Implementation recommendation
Initial implementation:
* `GenServer` queue;
* `Task.Supervisor`;
* running counters;
* retry tracking;
* process monitoring.
GenStage is not required for core workflows.
It may be considered later for Fleet if continuous producer-consumer backpressure becomes necessary.
---
# 33. RAG
## 33.1 Initial support
AgentEx supports agentic RAG through tools.
```elixir
defmodule SearchDocuments do
use AgentEx.Tool
name :search_documents
description "Retrieve relevant documents."
input do
field :query, :string, required: true
end
def call(%{query: query}, _context) do
MyVectorStore.search(query, top_k: 5)
end
end
```
## 33.2 Flow
```text
Model decides retrieval is needed
→ calls retrieval tool
→ tool returns chunks
→ chunks enter model context
→ model answers
```
## 33.3 Out of initial core scope
The first release does not need a large dedicated RAG subsystem for:
* ingestion;
* chunking;
* embedding;
* vector database management;
* document lifecycle management.
These may be added later as optional packages or helper behaviours.
---
# 34. Context and callbacks
## 34.1 Execution context
AgentEx should support a context map or struct containing application-owned data.
Examples:
```text
user_id
tenant_id
permissions
access_token
request_id
locale
metadata
```
The context should be available to:
* tools;
* MCP header callbacks;
* callbacks;
* providers where needed;
* approval policies.
## 34.2 Callback pipeline
Supported callbacks:
```text
before_run
after_run
before_model
after_model
before_tool
after_tool
before_step
after_step
```
Callbacks should be composable and should support halting execution.
---
# 35. Limits and safety controls
Agent and workflow execution should support:
```elixir
max_steps 20
max_tool_calls 10
timeout :timer.minutes(2)
max_cost_usd 0.50
```
Workflow-specific:
```elixir
max_visits :write, 5
```
Team-specific:
```elixir
max_rounds 5
```
Fleet-specific:
```elixir
max_running 100
max_queue 10_000
```
Additional future controls may include:
* maximum parallel branches;
* maximum tokens;
* maximum retries;
* provider-specific rate limits;
* tool allowlists;
* tool denylists;
* per-user budgets.
---
# 36. Telemetry and tracing
## 36.1 Telemetry events
AgentEx should emit `:telemetry` events by default. Telemetry can be disabled
application-wide:
```elixir
config :agent_ex, telemetry: false
```
An agent can override the application setting:
```elixir
defmodule QuietAgent do
use AgentEx, telemetry: false
end
```
Setting `use AgentEx, telemetry: true` on an agent enables its telemetry even
when the application-wide setting is disabled.
AgentEx should emit events such as:
```elixir
[:agent_ex, :run, :start]
[:agent_ex, :run, :stop]
[:agent_ex, :run, :exception]
[:agent_ex, :provider, :start]
[:agent_ex, :provider, :stop]
[:agent_ex, :provider, :exception]
[:agent_ex, :tool, :start]
[:agent_ex, :tool, :stop]
[:agent_ex, :tool, :exception]
[:agent_ex, :thread, :start]
[:agent_ex, :thread, :stop]
[:agent_ex, :checkpoint, :start]
[:agent_ex, :checkpoint, :stop]
[:agent_ex, :team, :message]
[:agent_ex, :workflow, :step, :start]
[:agent_ex, :workflow, :step, :stop]
[:agent_ex, :workflow, :split, :start]
[:agent_ex, :workflow, :join, :stop]
```
## 36.2 Metadata
Telemetry metadata should include where relevant:
* agent module;
* team module;
* workflow module;
* thread ID;
* provider;
* model;
* tool name;
* step name;
* split name;
* duration;
* token usage;
* cost;
* error category.
## 36.3 OpenTelemetry
OpenTelemetry support should be optional and built on top of `:telemetry`.
A tracing UI is not required for the first release.
---
# 37. Supervision
## 37.1 Application supervision
```elixir
def start(_type, _args) do
children = [
ResearchAgent,
SecurityAgent,
ReviewTeam,
ReviewWorkflow,
ResearchFleet
]
Supervisor.start_link(
children,
strategy: :one_for_one,
name: MyApp.Supervisor
)
end
```
Each top-level module exposes:
```elixir
child_spec/1
start_link/1
```
## 37.2 Internal responsibilities
Supervisors are responsible for process lifecycle.
Memory backends are responsible for state persistence.
Workers are responsible for loading their state during initialization.
## 37.3 Restoration
AgentEx should restore threads where:
```text
active = true
```
Restoration may initially be eager for active threads.
A later version may support:
```text
restore: :lazy
restore: :active
restore: :all
```
The definition of active must remain consistent.
---
# 38. Proposed module structure
## Core runtime
```text
AgentEx
AgentEx.Agent
AgentEx.Runtime
AgentEx.Runner
AgentEx.Loop
AgentEx.Context
AgentEx.Response
AgentEx.Error
```
## Providers
```text
AgentEx.Provider
AgentEx.Provider.Registry
AgentEx.Provider.Response
AgentEx.Provider.Stream
AgentEx.Provider.Local
AgentEx.Model
```
## Messages
```text
AgentEx.Message
AgentEx.Message.ToolCall
AgentEx.Message.ToolResult
```
## Tools
```text
AgentEx.Tool
AgentEx.Tool.Spec
AgentEx.Tool.Registry
AgentEx.Tool.Executor
AgentEx.Tool.Context
AgentEx.Tool.Result
AgentEx.Tool.Error
```
## MCP
```text
AgentEx.MCP
AgentEx.MCP.Client
AgentEx.MCP.Connection
AgentEx.MCP.Registry
AgentEx.MCP.Tool
AgentEx.MCP.Resource
AgentEx.MCP.Prompt
AgentEx.MCP.Transport
AgentEx.MCP.Transport.Stdio
AgentEx.MCP.Transport.HTTP
```
## Threads
```text
AgentEx.Thread
AgentEx.Thread.Server
AgentEx.Thread.Supervisor
AgentEx.Thread.Registry
AgentEx.Thread.Manager
AgentEx.Thread.State
AgentEx.Thread.ID
AgentEx.Thread.Metadata
```
## Memory and persistence
```text
AgentEx.Memory
AgentEx.Memory.Process
AgentEx.Memory.ETS
AgentEx.Persistence
AgentEx.Persistence.Policy
AgentEx.Persistence.None
AgentEx.Persistence.Always
AgentEx.Persistence.Checkpoint
AgentEx.Checkpoint
AgentEx.Checkpoint.Scheduler
```
## Approvals and interrupts
```text
AgentEx.Approval
AgentEx.Approval.Store
AgentEx.Approval.Decision
AgentEx.Interrupt
AgentEx.Interrupt.Store
```
## Teams
```text
AgentEx.Team
AgentEx.Team.Server
AgentEx.Team.Supervisor
AgentEx.Team.Registry
AgentEx.Team.Thread
AgentEx.Team.State
AgentEx.Team.Participant
AgentEx.Team.Glue
AgentEx.Team.Router
AgentEx.Team.Coordinator
AgentEx.Team.Broadcast
```
## Workflows
```text
AgentEx.Workflow
AgentEx.Workflow.Server
AgentEx.Workflow.Supervisor
AgentEx.Workflow.Registry
AgentEx.Workflow.State
AgentEx.Workflow.Step
AgentEx.Workflow.Transition
AgentEx.Workflow.Split
AgentEx.Workflow.Join
AgentEx.Workflow.Router
AgentEx.Workflow.Executor
AgentEx.Workflow.Result
AgentEx.Workflow.Checkpoint
```
Step adapters:
```text
AgentEx.Workflow.Step.Function
AgentEx.Workflow.Step.Agent
AgentEx.Workflow.Step.Team
AgentEx.Workflow.Step.Tool
AgentEx.Workflow.Step.Workflow
```
## Fleet
```text
AgentEx.Fleet
AgentEx.Fleet.Server
AgentEx.Fleet.Supervisor
AgentEx.Fleet.Queue
AgentEx.Fleet.Job
AgentEx.Fleet.Run
AgentEx.Fleet.Status
AgentEx.Fleet.Result
AgentEx.Fleet.Scheduler
AgentEx.Fleet.Worker
```
## Events and observability
```text
AgentEx.Event
AgentEx.Event.Stream
AgentEx.Event.Dispatcher
AgentEx.Telemetry
```
## Schemas
```text
AgentEx.Schema
AgentEx.Schema.Field
AgentEx.Schema.Validator
AgentEx.Schema.Error
```
---
# 39. Public APIs
## 39.1 Agent
```elixir
Agent.run(input, opts \\ [])
Agent.prompt(
thread_id_or_pid,
input,
opts \\ []
)
Agent.save()
Agent.save(thread_id_or_pid)
Agent.stop(thread_id_or_pid)
Agent.thread(thread_id)
Agent.active_threads()
Agent.alive_threads()
Agent.approve(approval_id, opts \\ [])
Agent.reject(approval_id, opts \\ [])
Agent.edit(approval_id, opts)
Agent.respond(approval_id, message)
```
## 39.2 Team
```elixir
Team.prompt(thread, input, opts \\ [])
Team.save()
Team.save(thread)
Team.stop(thread)
Team.thread(thread_id)
Team.active_threads()
Team.alive_threads()
Team.approve(approval_id, opts \\ [])
Team.reject(approval_id, opts \\ [])
Team.edit(approval_id, opts)
Team.respond(approval_id, message)
```
## 39.3 Workflow
```elixir
Workflow.run(input, opts \\ [])
Workflow.prompt(
thread,
input,
opts \\ []
)
Workflow.resume(
interrupt_id,
input,
opts \\ []
)
Workflow.save()
Workflow.save(thread)
Workflow.stop(thread)
Workflow.thread(thread_id)
Workflow.active_threads()
Workflow.alive_threads()
Workflow.approve(approval_id, opts \\ [])
Workflow.reject(approval_id, opts \\ [])
Workflow.edit(approval_id, opts)
Workflow.respond(approval_id, message)
```
## 39.4 Fleet
```elixir
Fleet.start(jobs, opts \\ [])
Fleet.status(run_id)
Fleet.results(run_id)
Fleet.stream(run_id)
Fleet.pause(run_id)
Fleet.resume(run_id)
Fleet.cancel(run_id, opts \\ [])
Fleet.retry_failed(run_id, opts \\ [])
Fleet.save()
Fleet.save(run_id)
```
---
# 40. Non-functional requirements
## 40.1 Performance
AgentEx should support thousands of idle or lightly active thread processes on a typical BEAM node.
The framework must avoid:
* one large global GenServer;
* serializing unrelated thread work;
* unnecessary process creation for one-off runs;
* copying full histories between multiple participant processes;
* unbounded model-call concurrency.
## 40.2 Fault isolation
A failure in one thread must not crash unrelated threads.
A failure in one workflow branch must be isolated and routed through workflow error handling.
A failure in one Fleet job must not terminate the entire Fleet run.
## 40.3 Determinism
Where concurrent results are merged, AgentEx must use deterministic rules.
For parallel workflow branches:
* updates to separate keys merge;
* shared-key updates require reducers;
* unresolved conflicts produce explicit errors.
## 40.4 Extensibility
Core behaviours should allow third parties to implement:
* providers;
* memory backends;
* persistence backends;
* MCP transports;
* tool schemas;
* event handlers;
* telemetry exporters.
## 40.5 Developer experience
Compile-time validation should catch:
* duplicate tool names;
* duplicate step names;
* unknown transition destinations;
* missing start steps;
* missing split joins where required;
* invalid join references;
* invalid provider names where statically known;
* malformed schemas.
Runtime errors should be typed and descriptive.
## 40.6 Documentation
Documentation must include:
* five-minute basic-agent guide;
* provider guide;
* tool guide;
* persistent-thread guide;
* approvals guide;
* team guide;
* workflow guide;
* Fleet guide;
* MCP guide;
* memory and persistence guide;
* Phoenix LiveView streaming example;
* production deployment guide.
---
# 41. First-release scope
## Required for v0.1
* `use AgentEx`;
* model-provider behaviour;
* local provider;
* provider registry;
* messages;
* module tools;
* function tools;
* arrays of tool declarations;
* agent loop;
* one-off `run`;
* persistent `prompt`;
* thread supervision;
* Registry lookup;
* process and ETS memory;
* custom memory behaviour;
* `:none`, `:always`, and checkpoint persistence;
* manual saving;
* active and alive thread states;
* approvals;
* streaming;
* telemetry;
* basic MCP client support;
* coordinated or broadcast Team;
* Workflow with:
* state;
* function steps;
* agent steps;
* team steps;
* tool steps;
* transitions;
* conditional transitions;
* cycles;
* named splits;
* joins;
* state reducers;
* interrupts;
* checkpoint resume.
## Candidate for v0.2
* Fleet;
* HTTP MCP improvements;
* MCP resources and prompts;
* cost budgets;
* richer structured outputs;
* provider-specific packages;
* Phoenix LiveView helpers;
* OpenTelemetry package;
* workflow visualization;
* improved thread restoration policies.
## Future scope
* AgentEx as MCP server;
* graph memory;
* vector-memory helpers;
* distributed Fleet;
* distributed workflow execution;
* durable external queue integrations;
* scheduling and cron;
* skills/plugin ecosystem;
* tracing UI;
* agent marketplace;
* browser automation package;
* coding-agent toolkit;
* channel adapters.
---
# 42. Explicitly out of scope for initial release
AgentEx v0.1 should not attempt to provide:
* a complete vector database;
* a graph database;
* a full RAG ingestion platform;
* a managed tracing SaaS;
* a hosted model gateway;
* a WhatsApp implementation;
* a complete OpenClaw product;
* a complete Claude Code product;
* a custom replacement for OTP supervisors;
* a custom distributed queue;
* a Cypher-compatible graph engine.
AgentEx should provide the runtime needed to build those applications.
---
# 43. Success metrics
## Developer adoption
* A developer can define and run a simple agent in under ten minutes.
* A developer can add a persistent thread without learning internal supervisors.
* A developer can convert a local tool into an MCP-backed tool without changing the agent loop.
* A developer can build a basic coordinated team in fewer than 30 lines of configuration.
* A developer can define a branching workflow without manually implementing process coordination.
## Reliability
* A crashing thread restarts without taking down unrelated threads.
* Persisted active threads restore correctly.
* Approval interruptions resume from the correct point.
* Workflow checkpoints restore active split and join state.
* Fleet concurrency remains within configured bounds.
## API simplicity
The number of primary concepts should remain limited to:
```text
Agent
Thread
Tool
Provider
Team
Workflow
Fleet
```
The public API should not require developers to manually construct:
* runners;
* executors;
* sessions;
* graph builders;
* orchestration contexts;
for ordinary use cases.
---
# 44. Acceptance criteria
## Basic agent
Given a valid provider and agent definition:
```elixir
ResearchAgent.run("Hello")
```
returns:
```elixir
{:ok, %AgentEx.Response{}}
```
with `thread_id: nil`.
## Persistent thread
Creating:
```elixir
ResearchAgent.prompt(
{:new, "thread-1"},
"Hello"
)
```
starts and registers a thread process.
Calling:
```elixir
ResearchAgent.prompt(
"thread-1",
"Continue"
)
```
uses the same persisted conversation state.
## Collision handling
Creating the same supplied thread ID twice returns:
```elixir
{:error, :thread_already_exists}
```
## Stop behavior
Calling:
```elixir
ResearchAgent.stop("thread-1")
```
persists inactive state before terminating the process.
## Approval
A required-approval tool does not execute until approved.
Editing an approval executes the edited arguments, not the original arguments.
## MCP
An imported MCP tool appears to the model as an ordinary AgentEx tool.
Approvals and callbacks work for MCP tools.
## Team
Every participant sees the same canonical team history.
Participant messages are committed with the participant identity.
A broadcast team stops when all participants pass or when `max_rounds` is reached.
## Workflow
A workflow step receives state and returns a partial update.
Conditional transitions choose the correct destination.
A named split starts selected branches in parallel.
A join waits according to its configured policy.
Concurrent writes without a reducer produce a state-conflict error.
An interrupted workflow can resume from its checkpoint.
## Fleet
Fleet never runs more than `max_running` jobs concurrently.
Failed jobs can be retried without rerunning successful jobs.
---
# 45. Final product model
```text
AgentEx
├── Provider
│ └── Connects models
├── Tool
│ └── Local functions, modules, or MCP
├── Agent
│ └── Reusable autonomous behavior
├── Thread
│ └── One stateful runtime instance
├── Team
│ └── One shared chat with many agents
├── Workflow
│ └── Explicit stateful execution
├── Fleet
│ └── Bulk scheduling and concurrency
├── Memory
│ └── State storage
├── Persistence
│ └── Direct or checkpointed durability
├── Approval
│ └── Human-controlled actions
├── MCP
│ └── External capabilities
└── Telemetry
└── Events, tracing, and monitoring
```
AgentEx should remain easy to understand:
```text
Agent
= what can act
Thread
= one ongoing instance
Team
= who collaborates
Workflow
= what happens next
Fleet
= how many run at once
Tool
= what they can do
Provider
= which model they use
```