Packages
Composable, validated actions for Elixir applications with built-in AI tool integration
Current section
Files
Jump to
Current section
Files
lib/jido_tools/workflow/execution.ex
defmodule Jido.Tools.Workflow.Execution do
@moduledoc false
alias Jido.Action.Error
alias Jido.Exec
alias Jido.Exec.Supervisors
alias Jido.Instruction
@spec execute_workflow(list(), map(), map(), module()) :: {:ok, map()} | {:error, any()}
def execute_workflow(steps, params, context, module) do
initial_acc = {:ok, params, %{}}
steps
|> Enum.reduce_while(initial_acc, &reduce_step(&1, &2, context, module))
|> case do
{:ok, _final_params, final_results} -> {:ok, final_results}
{:error, reason} -> {:error, reason}
end
end
defp reduce_step(step, {_status, current_params, results}, context, module) do
case module.execute_step(step, current_params, context) do
{:ok, step_result} when is_map(step_result) ->
updated_results = Map.merge(results, step_result)
updated_params = Map.merge(current_params, step_result)
{:cont, {:ok, updated_params, updated_results}}
{:ok, step_result} ->
{:halt,
{:error,
%{type: :invalid_step_result, message: "Expected map, got: #{inspect(step_result)}"}}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end
@doc false
@spec execute_step(tuple(), map(), map(), module()) :: {:ok, any()} | {:error, any()}
def execute_step(step, params, context, module) do
case step do
{:step, _metadata, [instruction]} ->
execute_instruction(instruction, params, context)
{:branch, metadata, [condition, true_branch, false_branch]} ->
execute_branch(condition, true_branch, false_branch, params, context, metadata, module)
{:converge, _metadata, [instruction]} ->
execute_instruction(instruction, params, context)
{:parallel, metadata, instructions} ->
execute_parallel(instructions, params, context, metadata, module)
_ ->
{:error, %{type: :invalid_step, message: "Unknown step type: #{inspect(step)}"}}
end
end
defp execute_instruction(instruction, params, context) do
case Instruction.normalize_single(instruction) do
{:ok, %Instruction{} = normalized} ->
run_normalized_instruction(normalized, params, context)
{:error, reason} ->
{:error,
%{
type: :invalid_instruction,
message: "Failed to normalize instruction: #{inspect(reason)}"
}}
end
end
defp run_normalized_instruction(%Instruction{} = normalized, params, context) do
merged_params = Map.merge(params, normalized.params)
merged_context = Map.merge(normalized.context, context)
instruction = %{normalized | params: merged_params, context: merged_context}
case Exec.run(instruction) do
{:ok, result} ->
{:ok, result}
{:ok, result, _other} ->
{:ok, result}
{:error, reason} ->
{:error, reason}
{:error, reason, _other} ->
{:error, reason}
end
end
defp execute_branch(condition, true_branch, false_branch, params, context, _metadata, module)
when is_boolean(condition) do
if condition do
module.execute_step(true_branch, params, context)
else
module.execute_step(false_branch, params, context)
end
end
defp execute_branch(
_condition,
_true_branch,
_false_branch,
_params,
_context,
metadata,
_module
) do
{:error,
%{
type: :invalid_condition,
message: "Invalid or unhandled condition in branch #{inspect(metadata)}"
}}
end
defp execute_parallel(instructions, params, context, metadata, module) do
max_concurrency = Keyword.get(metadata, :max_concurrency, System.schedulers_online())
# Extract jido instance from context if present (set by parent workflow)
jido_opts = if context[:__jido__], do: [jido: context[:__jido__]], else: []
# Resolve supervisor based on jido: option (defaults to global)
task_sup = Supervisors.task_supervisor(jido_opts)
stream_opts = [
ordered: true,
max_concurrency: max_concurrency,
timeout: :infinity,
on_timeout: :kill_task
]
results =
Task.Supervisor.async_stream(
task_sup,
instructions,
fn instruction ->
execute_parallel_instruction(instruction, params, context, module)
end,
stream_opts
)
|> Enum.map(&handle_stream_result/1)
{:ok, %{parallel_results: results}}
end
defp handle_stream_result({:ok, value}), do: value
defp handle_stream_result({:exit, reason}) do
%{error: Error.execution_error("Parallel task exited", %{reason: reason})}
end
defp execute_parallel_instruction(instruction, params, context, module) do
case module.execute_step(instruction, params, context) do
{:ok, result} -> result
{:error, reason} -> %{error: reason}
end
rescue
e ->
%{error: Error.execution_error("Parallel step raised", %{exception: e})}
catch
kind, reason ->
%{error: Error.execution_error("Parallel step caught", %{kind: kind, reason: reason})}
end
end