Excellent question. This refactoringâextracting jido_action
into a standalone libraryâis a brilliant architectural move. It isolates a core pattern and allows us to reason about its place in the wider system.
If we only have the jido_action
library, its role becomes crystal clear: it is a universal, standardized “Command Pattern” for the entire stack. It provides a way to define, validate, execute, and compose discrete units of work.
Here is a detailed analysis of its place and a robust integration plan.
Part 1: Architectural Placement of jido_action
jido_action
is not infrastructure, nor is it a complete application framework. It is a powerful, reusable pattern library. It sits neatly between the low-level infrastructure of foundation
and the high-level application logic of ElixirML
.
This creates a clean, four-layer architecture:
(Future runtime that consumes Jido.Action)"] end subgraph "Layer 2: jido_action (The Command Pattern)" L2A["\`Jido.Action\` (Behavior for defining commands)"] L2B["\`Jido.Exec\` (Engine for running commands)"] L2C["\`Jido.Instruction\` (A specific command instance)"] L2D["Pre-built Actions (Files, Arithmetic, etc.)"] end subgraph "Layer 1: Foundation (The BEAM OS Kernel)" L1A["\`Foundation.Core\`
(Process/Service Registries)"] L1B["\`Foundation.Infrastructure\`
(Circuit Breaker, Rate Limiter)"] L1C["\`Foundation.Observability\`
(Events, Telemetry)"] L1D["\`Foundation.Coordination.Primitives\`
(Consensus, Barriers)"] end L4A --> L2A L3A --> L2A L2A --> L1B L2A --> L1C style L2A fill:#e1f5fe,stroke:#01579b,stroke-width:4px,color:#000
Key Insights:
foundation
is Layer 1: It’s the “kernel” that knows nothing about application logic. It manages processes, provides resilience patterns (CircuitBreaker
), and handles observability (Telemetry
).jido_action
is Layer 2: It’s a utility library that provides a standardized way to define and execute functions. It sits on top offoundation
because anAction
’srun/2
implementation might need to usefoundation
’s infrastructure (e.g., wrap a network call in a circuit breaker).jido
(the full agent framework) is Layer 3: The fulljido
agent library (which you’ve refactoredjido_action
out of) would be the next layer up. AJido.Agent
would useJido.Exec
to run its queuedJido.Instruction
s.ElixirML
is Layer 4: It’s the application. It defines ML-specificAction
s (e.g.,GenerateCodeAction
) and uses the agent framework (jido
) or the command executor (Jido.Exec
) to run them.
Part 2: Robust Integration Plan
This plan details how the jido_action
library would be wired into the existing foundation
and ElixirML
stacks.
Step 1: Establish the Dependency Hierarchy
The dependency chain in mix.exs
would be crystal clear:
elixir_ml
depends onjido_action
andfoundation
.jido_action
depends onfoundation
.foundation
has no application-level dependencies.
# In ElixirML's mix.exs
def deps do
[
{:jido_action, "~> x.y"},
{:foundation, "~> x.y"},
# ... other deps
]
end
# In jido_action's mix.exs
def deps do
[
{:foundation, "~> x.y"},
# ... other deps
]
end
Step 2: Connect jido_action
Down to foundation
The Jido.Action
pattern becomes more powerful when it can leverage foundation
’s infrastructure. Actions are no longer just pure functions; they are resilient, observable operations.
Example: A Foundation-Aware Action
Let’s imagine an Action in ElixirML
that needs to call an external API. It would use Foundation.Infrastructure.CircuitBreaker
.
# in elixir_ml/actions/call_external_api_action.ex
defmodule ElixirML.Actions.CallExternalAPI do
# Use the behavior from the standalone library
use Jido.Action,
name: "call_external_api",
schema: [
url: [type: :string, required: true],
payload: [type: :map, required: true]
]
# Depend on foundation's infrastructure
alias Foundation.Infrastructure.{CircuitBreaker, ConnectionManager}
alias Foundation.Infrastructure.PoolWorkers.HttpWorker
@impl Jido.Action
def run(params, _context) do
# 1. Use Foundation's Circuit Breaker
CircuitBreaker.execute(:external_llm_service, fn ->
# 2. Use Foundation's Connection Pool
ConnectionManager.with_connection(:http_pool, fn http_worker ->
# 3. Use a pooled worker to make the call
HttpWorker.post(http_worker, params.url, params.payload)
end)
end)
# The result is automatically a standard {:ok, _} or {:error, Foundation.Types.Error}
end
end
Benefits of this integration:
- Resilience: The action automatically gains circuit breaking and connection pooling without needing to manage that logic itself.
- Observability:
Foundation.CircuitBreaker
andConnectionManager
already have built-in telemetry. Every execution of this action is now automatically monitored. - Standardization: The action conforms to the
Jido.Action
behavior, but its implementation is powered byfoundation
.
Step 3: Connect ElixirML
Up to jido_action
ElixirML
programs and modules would use Jido.Exec.run/2
as their primary method for executing complex, stateful, or external operations.
Example: An ElixirML Program Using an Action
# in elixir_ml/programs/feature_implementer.ex
defmodule ElixirML.Programs.FeatureImplementer do
use DSPEx.Module # Or your program abstraction
alias Jido.Exec
alias ElixirML.Actions.{GenerateCodeAction, WriteAndTestAction}
# The main logic of the program
def forward(program_state, inputs) do
# ... logic to determine task ...
task_spec = %{requirements: inputs.requirements, language: "Elixir"}
# Use Jido.Exec to run a chain of actions
Jido.Exec.Chain.chain(
[
# Run the action to generate code
{GenerateCodeAction, task_spec},
# Run another action to write the file and run tests
{WriteAndTestAction, %{file_path: "lib/new_feature.ex"}}
],
%{}, # Initial params
program_state.context # Pass context down
)
end
end
Benefits of this integration:
- Decoupling: The
FeatureImplementer
program doesn’t need to know how to generate code or run tests. It only needs to know whichAction
to call. - Testability:
GenerateCodeAction
can be tested in isolation. TheFeatureImplementer
program can be tested by mocking the output ofJido.Exec.Chain.chain/3
. - Clarity: The
forward/2
function becomes a high-level description of a workflow, composed of well-definedAction
s.
Step 4: Refactor jido_action
to Use Foundation.Error
The standalone jido_action
library defines its own error struct (Jido.Action.Error
). To create a fully cohesive stack, it should be refactored to use Foundation.Types.Error
and the helpers in Foundation.Error
.
- In
jido_action/mix.exs
: Add{:foundation, ...}
dependency. - In
jido_action/error.ex
: Remove theJido.Action.Error
struct and its helpers. - In
jido_action.ex
andjido_exec.ex
:- Alias
Foundation.Types.Error
andFoundation.Error
. - Replace all calls like
Jido.Action.Error.validation_error(...)
with the equivalentFoundation.Error.new(:validation_failed, ...)
or a more specific code.
- Alias
This change ensures that an error, no matter where it originates (foundation
, jido_action
, or ElixirML
), has a single, consistent, and richly detailed structure.
Part 3: Evolutionary Path - The Emergence of the Full Agent
This refactored architecture provides a perfect base for re-introducing the full Jido
agent framework as Layer 3.
- A new
jido_agent
library would be created. - It would depend on
jido_action
andfoundation
. - The
Jido.Agent.Server
from the originaljido
repo would be the heart of this new library. - Its
run
implementation would useJido.Exec.run
to execute the instructions from its queue. - The
Directives.ex
andStateManager.ex
actions, which were commented out ofjido_action
, would find their home here, as they are specific to a stateful agent runtime.
An agent from this new library would look like this:
# In a new `jido_agent` library:
defmodule Jido.Agent do
use GenServer
alias Jido.Instruction
alias Jido.Exec
alias Foundation.ServiceRegistry
# --- Agent Server Logic ---
def start_link(agent_module, opts) do
# Uses Foundation's registry
via_tuple = ServiceRegistry.via_tuple(:production, agent_module.name())
GenServer.start_link(__MODULE__, {agent_module, opts}, name: via_tuple)
end
def init({agent_module, opts}) do
# ... agent setup ...
state = %{
agent_module: agent_module,
instruction_queue: :queue.new(),
# ... other state
}
{:ok, state, {:continue, :run_next_instruction}}
end
def handle_continue(:run_next_instruction, state) do
case :queue.out(state.instruction_queue) do
{{:value, instruction}, new_queue} ->
# It uses Jido.Exec to run the command!
case Exec.run(instruction) do
# ... handle result and directives ...
end
{:noreply, %{state | instruction_queue: new_queue}, {:continue, :run_next_instruction}}
{:empty, _} ->
# Queue is empty, wait for more work
{:noreply, state}
end
end
# ... other callbacks for enqueuing instructions, etc.
end
Conclusion
The refactoring of jido
into jido_action
was a pivotal step towards a cleaner architecture.
If we only had jido_action
, it would become the universal “Command Pattern” library for our stack.
- Placement: It would be Layer 2, sitting directly on top of
foundation
. - Dependencies: It would depend on
foundation
to use its infrastructure primitives likeCircuitBreaker
andError
. - Role: It provides a standard, observable, and resilient way to define and execute any business logic.
- Integration:
- Downstream: Both
foundation
services andElixirML
programs can useJido.Exec
to run encapsulated logic. - Upstream: A full agent framework (
jido
orMABEAM
) can be built on top, usingJido.Exec
as its core instruction-execution engine.
- Downstream: Both
This model is clean, composable, and aligns perfectly with the Elixir philosophy of building powerful systems from small, well-defined, and reusable components.