AgentEx Product Requirements Document

Copy Markdown

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:

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:

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:

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:

defmodule ResearchAgent do
  use AgentEx

  model "openai:gpt-5"
  system "You are a research assistant."

  tools [
    SearchWeb
  ]
end

And should run through:

ResearchAgent.run("Research Phoenix.")

4.2 OTP-native

AgentEx must reuse OTP rather than recreate it.

It should use:

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:

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.

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.

ResearchAgent.run("Research Phoenix.")

7.2 Stateful assistant

Maintain a persistent conversation.

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.

tool SendEmail,
  approval: :required

7.4 Agentic RAG

Expose document retrieval as a normal tool.

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

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:

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.

AgentEx.Provider
AgentEx.Provider.Registry
AgentEx.Provider.Response
AgentEx.Provider.Stream
AgentEx.Provider.Local

Optional provider packages expose:

AgentEx.Provider.OpenAI
AgentEx.Provider.Anthropic
AgentEx.Provider.Google
AgentEx.Provider.OpenRouter

9.2 Provider resolution

model "openai:gpt-5"

is parsed into:

%AgentEx.Model{
  provider: AgentEx.Provider.OpenAI,
  name: "gpt-5",
  options: []
}

Provider resolution order:

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:

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.

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

defmodule MyApp.CustomProvider do
  @behaviour AgentEx.Provider

  @impl true
  def call(model, messages, tools, opts) do
    ...
  end
end

Direct usage:

model MyApp.CustomProvider,
  name: "reasoner-v2"

Registered usage:

config :agent_ex, :providers,
  acme: MyApp.CustomProvider
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

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 %{}.

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.

RepoStats → repo_stats

10.2 Function tools

tool :search_docs,
  description: "Search internal documents.",
  input: [
    query: [type: :string, required: true]
  ],
  call: &search_docs/2

Anonymous function:

tool :current_time,
  description: "Return the current application time.",
  input: [],
  call: fn _args, _context ->
    DateTime.utc_now()
  end

10.3 Multiple tool declarations

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:

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:

before_call &validate/1
after_call &audit/1

Registration callbacks:

tool SendMessage,
  before_call: &check_permissions/1,
  after_call: &record_delivery/1

Agent-level callbacks:

before_tool &log_tool/1
after_tool &record_tool_result/1

Callbacks should return either:

context

or:

{:halt, reason}

10.5 Tool execution options

tool SearchGitHub,
  timeout: 15_000,
  retries: 2,
  backoff: :exponential

10.6 Tool normalization

All tools should become:

%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

mcp :github,
  command: "github-mcp-server",
  args: ["stdio"]

11.3 Remote MCP server

mcp :internal,
  url: "https://mcp.example.com",
  headers: fn context ->
    [
      {"authorization", "Bearer #{context.access_token}"}
    ]
  end

11.4 MCP tool declaration

tool :search_repositories,
  mcp: :github

Rename locally:

tool :github_search,
  mcp: :github,
  remote: :search_repositories

Normal tool options remain available:

tool :create_issue,
  mcp: :github,
  approval: :required,
  timeout: 30_000,
  retries: 2

11.5 Import MCP tools

Import all:

tools [
  {:mcp, :github}
]

Import selected:

tools [
  {:mcp, :github,
   only: [
     :search_repositories,
     :get_issue,
     :create_issue
   ]}
]

Exclude tools:

tools [
  {:mcp, :github,
   except: [:delete_repository]}
]

11.6 Reusable MCP module

defmodule MyApp.GitHubMCP do
  use AgentEx.MCP

  transport :stdio,
    command: "github-mcp-server",
    args: ["stdio"]
end

Usage:

tools [
  {MyApp.GitHubMCP,
   only: [:search_repositories, :get_issue]}
]

11.7 MCP tool execution

MCP tools should normalize into ordinary tool specs:

%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

%AgentEx.Message{
  role: :user,
  content: "Hello",
  metadata: %{}
}

Supported roles:

:system
:user
:assistant
:tool

12.2 Constructors

AgentEx.Message.system(content)
AgentEx.Message.user(content)
AgentEx.Message.assistant(content)
AgentEx.Message.tool(content, opts)

12.3 Multiple-message input

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

ResearchAgent.run("What is Phoenix?")

Default behavior:

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:

%AgentEx.Response{
  content: "Phoenix is...",
  message: %AgentEx.Message{},
  thread_id: nil,
  tool_calls: [],
  usage: %{}
}

13.2 Run against stored state

ResearchAgent.run(
  "Summarize the thread.",
  thread: {:existing, "phoenix"},
  save: false
)

Persist the resulting state:

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:

ResearchAgent.prompt(
  :new,
  "Research Phoenix."
)

Supplied ID:

ResearchAgent.prompt(
  {:new, "phoenix"},
  "Research Phoenix."
)

Continue:

ResearchAgent.prompt(
  "phoenix",
  "Who maintains it?"
)

By PID:

ResearchAgent.prompt(
  pid,
  "Continue."
)

13.4 Thread collision behavior

Creating a supplied thread ID that already exists must return:

{:error, :thread_already_exists}

Continuing an unknown thread must return:

{:error, :thread_not_found}

13.5 Thread ID generator

Agents should support a custom thread ID generator.

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:

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:

children = [
  ResearchAgent
]

should start the agent’s internal runtime infrastructure.

Conceptually:

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:

{ResearchAgent, thread_id}

15.3 Thread lookup behavior

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:

idle thread       → start a new run
running thread    → queue prompt
interrupted thread→ await approval or resume input

A later version may allow configurable policies:

: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:

active: true
alive: true

Immediately after restart:

active: true
alive: false

After restoration:

active: true
alive: true

16.4 In-memory backend restart behavior

With:

memory: :memory

all thread state and lifecycle metadata are lost after a full application restart.

No thread can be restored.

16.5 Stop behavior

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

ResearchAgent.active_threads()
ResearchAgent.alive_threads()
ResearchAgent.thread("phoenix")

17. Memory

17.1 Process memory

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

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

memory: MyApp.AgentMemory

Suggested behaviour:

@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:

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

persistence: :none

No automatic persistence of conversation state.

Manual save remains possible where a backend supports it.

18.2 Always

persistence: :always

Persist every state change.

18.3 Checkpoint

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:

:approval
:interrupt
:tool_call
:stop
:end

18.4 Lifecycle metadata

Lifecycle metadata must persist immediately regardless of conversation-state policy.

Examples:

active
inactive
created_at
updated_at

Conversation state follows the configured persistence policy.

Examples:

messages
tool history
agent state
workflow state
team history

18.5 Manual saving

ResearchAgent.save()
ResearchAgent.save("phoenix")
ResearchAgent.save(pid)

save/0 saves all dirty threads for the agent.


19. Approvals

19.1 Tool declaration

tool SendMessage,
  approval: :required

19.2 Approval result

When the model requests the tool:

{:approval_required, approval}

19.3 Approval structure

%AgentEx.Approval{
  id: "approval-123",
  agent: ResearchAgent,
  thread_id: "phoenix",
  tool: "send_message",
  arguments: %{}
}

19.4 Supported decisions

Approve:

ResearchAgent.approve(approval.id)

Reject:

ResearchAgent.reject(
  approval.id,
  reason: "Do not send it."
)

Edit and execute:

ResearchAgent.edit(
  approval.id,
  arguments: updated_arguments
)

Respond to model:

ResearchAgent.respond(
  approval.id,
  "Rewrite the message."
)

19.5 Allowed decisions

Tools may restrict available approval decisions.

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

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:

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.

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

%AgentEx.Message{
  role: :assistant,
  sender: :security,
  content: "I found one dependency concern."
}

21.4 Team modes

Teams support:

:coordinated
:broadcast

22. Coordinated teams

22.1 Definition

defmodule ReviewTeam do
  use AgentEx.Team

  participants [
    researcher: ResearchAgent,
    security: SecurityAgent,
    writer: WriterAgent
  ]

  coordinator :researcher
end

22.2 Flow

User message
→ coordinator
→ coordinator delegates work
→ participants post updates
→ coordinator decides when finished
→ final response

22.3 Delegation results

Coordinator actions may include:

{:delegate,
 [
   {:researcher, "Check repository activity."},
   {:security, "Check known advisories."}
 ]}

Parallel delegation:

{:parallel,
 [
   {:researcher, "Inspect maintenance activity."},
   {:security, "Inspect vulnerabilities."}
 ]}

Finish:

{:done, "Final answer"}

23. Broadcast teams

23.1 Definition

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:

Participant system prompt
+ team glue prompt
+ participant glue override
+ canonical shared history
+ new-message event

23.3 Broadcast flow

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

{:message, "I found two risks."}
:pass
:done

Targeted result:

{: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

participant :security, SecurityAgent,
  receive_from: [:user, :researcher],
  glue: """
  Interpret updates for security implications.
  """,
  max_responses: 3

Array form:

participants [
  {:researcher, ResearchAgent},

  {:security, SecurityAgent,
   receive_from: [:user, :researcher]},

  {:writer, WriterAgent,
   receive_from: :all}
]

23.7 Team API

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:

workflow
state
step
transition
split
join

24.2 Workflow state

The workflow owns its state.

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

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

step :prepare, &prepare/1
def prepare(state) do
  %{repository: String.trim(state.repository)}
end

25.2 Inline step

step :prepare, fn state ->
  %{repository: String.trim(state.repository)}
end

25.3 Agent step

step :research,
  agent: ResearchAgent,
  input: &research_prompt/1,
  output: :research

Agent steps should be stateless by default.

Internally they call:

ResearchAgent.run(prompt)

The workflow remains the state source of truth.

25.4 Stateful agent step

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

step :review,
  team: ReviewTeam,
  input: &review_prompt/1,
  output: :review

25.6 Tool step

step :notify,
  tool: SendMessage,
  input: &notification_input/1

25.7 Sub-workflow step

step :audit,
  workflow: AuditWorkflow,
  input: &audit_input/1,
  output: :audit

26. Workflow input and output mapping

26.1 Input mapping

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

output: :security

This stores the normalized response under state.security.

26.3 Output function

output: fn response, state ->
  %{
    security: response.content,
    critical?: response.metadata[:critical?] || false
  }
end

26.4 Ignore output

output: :ignore

27. Workflow transitions

27.1 Start

start :research

27.2 Normal transition

transition :research, to: :security
transition :report, to: :end

27.3 Conditional transition

transition :security,
  when: &critical?/1,
  to: :escalate

transition :security,
  otherwise: :report

Conditions are evaluated in declaration order.

27.4 Dynamic transition

transition :classify,
  to: &route/1
def route(state) do
  cond do
    state.critical? -> :escalate
    state.needs_review? -> :review
    true -> :report
  end
end

27.5 Cycles

transition :write, to: :review

transition :review,
  when: &needs_revision?/1,
  to: :write

transition :review,
  otherwise: :end

Cycle protection:

max_steps 50
max_visits :write, 5

28. Named parallel splits and joins

28.1 Named split

split :review_checks,
  from: :research,
  to: [:security, :quality]

The split name prevents the caller from having to repeat branch lists manually.

28.2 Join

join :review_checks,
  to: :combine,
  wait: :all

Supported join policies:

wait: :all
wait: :any
wait: {:count, 2}

Default:

wait: :all

28.3 Conditional split

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.

%{security: result}
%{quality: result}

Concurrent writes to the same key require a reducer.

field :findings, {:array, :map},
  default: [],
  reducer: &Kernel.++/2

Without a reducer:

{:error, {:state_conflict, :findings}}

28.6 Runtime split state

Conceptually:

%AgentEx.Workflow.Split{
  name: :review_checks,
  selected_branches: [:security, :quality],
  completed: MapSet.new(),
  results: %{}
}

29. Workflow interrupts

29.1 Create interrupt

def review(state) do
  AgentEx.Workflow.interrupt(
    :human_review,
    %{report: state.report}
  )
end

Result:

{:interrupted, interrupt}

29.2 Resume

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

step :security,
  agent: SecurityAgent,
  retries: 3,
  backoff: :exponential,
  timeout: 30_000

30.2 Error transition

transition :security,
  on_error: :security_failed

30.3 Global error handler

on_error &handle_error/2

Possible return values:

{:retry, opts}
{:continue, next_step, state_update}
{:halt, reason}

31. Workflow API

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

defmodule ResearchFleet do
  use AgentEx.Fleet,
    agent: ResearchAgent,
    max_running: 100,
    max_queue: 10_000
end

Potential targets:

agent: ResearchAgent
team: ReviewTeam
workflow: ReviewWorkflow

32.3 Jobs

jobs =
  for id <- 1..4_000 do
    %{
      thread: {:new, "thread-#{id}"},
      input: "Research project #{id}"
    }
  end

32.4 Start

{:ok, run} =
  ResearchFleet.start(jobs)

32.5 Run state

%AgentEx.Fleet.Run{
  id: "fleet-run-123",
  total: 4_000,
  queued: 3_900,
  running: 100,
  completed: 0,
  failed: 0
}

32.6 Control API

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:

4,000 alive threads
≠
4,000 simultaneous model calls

Fleet should enforce bounded provider and tool concurrency.

32.8 Implementation recommendation

Initial implementation:

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.

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

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:

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:

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:

max_steps 20
max_tool_calls 10
timeout :timer.minutes(2)
max_cost_usd 0.50

Workflow-specific:

max_visits :write, 5

Team-specific:

max_rounds 5

Fleet-specific:

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:

config :agent_ex, telemetry: false

An agent can override the application setting:

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:

[: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

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:

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:

active = true

Restoration may initially be eager for active threads.

A later version may support:

restore: :lazy
restore: :active
restore: :all

The definition of active must remain consistent.


38. Proposed module structure

Core runtime

AgentEx
AgentEx.Agent
AgentEx.Runtime
AgentEx.Runner
AgentEx.Loop
AgentEx.Context
AgentEx.Response
AgentEx.Error

Providers

AgentEx.Provider
AgentEx.Provider.Registry
AgentEx.Provider.Response
AgentEx.Provider.Stream
AgentEx.Provider.Local
AgentEx.Model

Messages

AgentEx.Message
AgentEx.Message.ToolCall
AgentEx.Message.ToolResult

Tools

AgentEx.Tool
AgentEx.Tool.Spec
AgentEx.Tool.Registry
AgentEx.Tool.Executor
AgentEx.Tool.Context
AgentEx.Tool.Result
AgentEx.Tool.Error

MCP

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

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

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

AgentEx.Approval
AgentEx.Approval.Store
AgentEx.Approval.Decision

AgentEx.Interrupt
AgentEx.Interrupt.Store

Teams

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

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:

AgentEx.Workflow.Step.Function
AgentEx.Workflow.Step.Agent
AgentEx.Workflow.Step.Team
AgentEx.Workflow.Step.Tool
AgentEx.Workflow.Step.Workflow

Fleet

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

AgentEx.Event
AgentEx.Event.Stream
AgentEx.Event.Dispatcher
AgentEx.Telemetry

Schemas

AgentEx.Schema
AgentEx.Schema.Field
AgentEx.Schema.Validator
AgentEx.Schema.Error

39. Public APIs

39.1 Agent

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

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

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

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:

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:

ResearchAgent.run("Hello")

returns:

{:ok, %AgentEx.Response{}}

with thread_id: nil.

Persistent thread

Creating:

ResearchAgent.prompt(
  {:new, "thread-1"},
  "Hello"
)

starts and registers a thread process.

Calling:

ResearchAgent.prompt(
  "thread-1",
  "Continue"
)

uses the same persisted conversation state.

Collision handling

Creating the same supplied thread ID twice returns:

{:error, :thread_already_exists}

Stop behavior

Calling:

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

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:

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