From 3e207685fe6eefada10274e86f90f1a00b16c098 Mon Sep 17 00:00:00 2001 From: nshkrdotcom <127063941+nshkrdotcom@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:31:41 -1000 Subject: [PATCH] refactor: decompose large macros and improve code organization - Refactored `Jido.Agent.__using__/1` macro by extracting quoted code into separate helper functions to avoid Credo "long quote blocks" and "nested too deep" warnings. Compile-time validation and runtime code are now clearly separated. - Applied similar decomposition to `Jido.Skill.__using__/1` macro, breaking it into focused helper functions for behaviour setup, accessors, and callbacks. - Extracted complex conditional logic into private helper functions across multiple modules (AgentServer, Cron directive, Await, Routes, etc.) to improve readability and reduce nesting depth. - Standardized alias usage throughout codebase: added module aliases at top of files and replaced inline module references with aliased versions. - Fixed idle timer handling in Keyed lifecycle to use `:erlang.start_timer/3` with timer refs instead of `Process.send_after/3`, preventing stale timeout messages from triggering after cancel/reset. - Added `restart: :transient` to InstanceManager child specs to avoid immediate restarts on normal shutdown/idle timeout while still allowing restarts on crashes. - Added logger configuration for Jido telemetry metadata keys. - Replaced `length(list) >= N` comparisons with pattern matching or `!= []` in tests for more idiomatic Elixir. - Used `Enum.map_join/3` instead of `Enum.map/2 |> Enum.join/2` where applicable. - Reordered aliases alphabetically in test files for consistency. --- config/config.exs | 19 + lib/jido.ex | 4 +- lib/jido/actions/control.ex | 21 +- lib/jido/agent.ex | 558 +++++++++++------- lib/jido/agent/directive/cron.ex | 87 +-- lib/jido/agent/instance_manager.ex | 9 +- lib/jido/agent/state_ops.ex | 5 +- lib/jido/agent/strategy.ex | 13 +- lib/jido/agent/strategy/fsm.ex | 19 +- lib/jido/agent_server.ex | 239 ++++---- lib/jido/agent_server/directive_executors.ex | 48 +- lib/jido/agent_server/error_policy.ex | 5 +- lib/jido/agent_server/lifecycle/keyed.ex | 9 +- lib/jido/await.ex | 28 +- lib/jido/igniter/templates.ex | 5 +- lib/jido/observe.ex | 8 +- lib/jido/sensor/runtime.ex | 6 +- lib/jido/skill.ex | 123 +++- lib/jido/skill/instance.ex | 4 +- lib/jido/skill/requirements.ex | 6 +- lib/jido/skill/routes.ex | 42 +- lib/jido/util.ex | 4 +- lib/mix/tasks/jido.gen.agent.ex | 15 +- lib/mix/tasks/jido.gen.sensor.ex | 13 +- lib/mix/tasks/jido.gen.skill.ex | 20 +- lib/mix/tasks/jido.install.ex | 9 +- test/examples/emit_directive_test.exs | 4 +- test/examples/error_handling_test.exs | 2 +- test/examples/hierarchical_agents_test.exs | 16 +- test/examples/observability_test.exs | 4 +- test/examples/parent_child_test.exs | 10 +- test/examples/schedule_directive_test.exs | 2 +- test/examples/sensor_demo_test.exs | 12 +- test/examples/signal_routing_test.exs | 2 +- test/examples/spawn_agent_test.exs | 6 +- test/examples/tracing_test.exs | 10 +- test/jido/agent/agent_test.exs | 4 +- test/jido/agent/directive_test.exs | 5 +- test/jido/agent/instance_manager_test.exs | 3 +- test/jido/agent/state_ops_test.exs | 4 +- .../agent_server_coverage_test.exs | 11 +- .../agent_server_stop_log_test.exs | 2 +- test/jido/agent_server/agent_server_test.exs | 2 +- .../agent_server/cron_integration_test.exs | 2 +- .../jido/agent_server/directive_exec_test.exs | 2 +- test/jido/agent_server/error_policy_test.exs | 2 +- test/jido/agent_server/hierarchy_test.exs | 7 +- test/jido/agent_server/signal_router_test.exs | 3 +- .../agent_server/skill_subscriptions_test.exs | 4 +- test/jido/agent_server/strategy_init_test.exs | 2 +- test/jido/agent_server/telemetry_test.exs | 2 +- .../agent_server/trace_propagation_test.exs | 2 +- test/jido/agent_skill_integration_test.exs | 5 +- test/jido/await_coverage_test.exs | 2 +- test/jido/await_test.exs | 2 +- test/jido/discovery_test.exs | 4 +- test/jido/error_coverage_test.exs | 3 +- test/jido/error_test.exs | 2 +- test/jido/observe/observe_coverage_test.exs | 13 +- test/jido/skill/routes_test.exs | 2 +- test/jido/skill/schedules_test.exs | 5 +- test/jido/telemetry_test.exs | 2 +- test/jido/tracing/trace_test.exs | 2 +- test/support/eventually.ex | 14 +- test/support/test_agents.ex | 4 +- 65 files changed, 929 insertions(+), 575 deletions(-) diff --git a/config/config.exs b/config/config.exs index 28c0a02..6605eaf 100644 --- a/config/config.exs +++ b/config/config.exs @@ -2,6 +2,25 @@ import Config config :jido, default: Jido.DefaultInstance +# Logger configuration for Jido telemetry metadata +# These metadata keys are used by Jido.Telemetry for structured logging +config :logger, :default_formatter, + format: "[$level] $message $metadata\n", + metadata: [ + :agent_id, + :agent_module, + :action, + :directive_count, + :directive_type, + :duration_μs, + :error, + :instruction_count, + :queue_size, + :result, + :signal_type, + :strategy + ] + # Git hooks and git_ops configuration for conventional commits # Only enabled in dev environment (git_ops is a dev-only dependency) if config_env() == :dev do diff --git a/lib/jido.ex b/lib/jido.ex index 8bcc724..966d019 100644 --- a/lib/jido.ex +++ b/lib/jido.ex @@ -1,6 +1,8 @@ defmodule Jido do use Supervisor + alias Jido.Agent.WorkerPool + @moduledoc """ 自動 (Jido) - A foundational framework for building autonomous, distributed agent systems in Elixir. @@ -201,7 +203,7 @@ defmodule Jido do ] pool_children = - Jido.Agent.WorkerPool.build_pool_child_specs(name, Keyword.get(opts, :agent_pools, [])) + WorkerPool.build_pool_child_specs(name, Keyword.get(opts, :agent_pools, [])) Supervisor.init(base_children ++ pool_children, strategy: :one_for_one) end diff --git a/lib/jido/actions/control.ex b/lib/jido/actions/control.ex index 0efdd62..0d7367e 100644 --- a/lib/jido/actions/control.ex +++ b/lib/jido/actions/control.ex @@ -109,14 +109,29 @@ defmodule Jido.Actions.Control do def run(%{target_pid: pid, signal_type: type, payload: payload, source: source}, context) do original = context[:signal] - final_type = type || (original && original.type) || "forwarded" - final_payload = payload || (original && original.data) || %{} - final_source = source || ((original && original.source) || "") <> "/forwarded" + final_type = resolve_type(type, original) + final_payload = resolve_payload(payload, original) + final_source = resolve_source(source, original) signal = Signal.new!(final_type, final_payload, source: final_source) directive = Directive.emit_to_pid(signal, pid) {:ok, %{forwarded_to: pid}, [directive]} end + + defp resolve_type(type, _original) when is_binary(type), do: type + defp resolve_type(nil, %{type: type}) when is_binary(type), do: type + defp resolve_type(nil, _original), do: "forwarded" + + defp resolve_payload(payload, _original) when is_map(payload), do: payload + defp resolve_payload(nil, %{data: data}) when is_map(data), do: data + defp resolve_payload(nil, _original), do: %{} + + defp resolve_source(source, _original) when is_binary(source), do: source + + defp resolve_source(nil, %{source: original_source}) when is_binary(original_source), + do: original_source <> "/forwarded" + + defp resolve_source(nil, _original), do: "/forwarded" end defmodule Broadcast do diff --git a/lib/jido/agent.ex b/lib/jido/agent.ex index b0f874e..b023218 100644 --- a/lib/jido/agent.ex +++ b/lib/jido/agent.ex @@ -129,12 +129,14 @@ defmodule Jido.Agent do Server/OTP integration is handled separately by `Jido.AgentServer`. """ + alias Jido.Action.Schema alias Jido.Agent alias Jido.Agent.Directive alias Jido.Agent.State, as: StateHelper - alias Jido.Action.Schema alias Jido.Error alias Jido.Instruction + alias Jido.Skill.Instance, as: SkillInstance + alias Jido.Skill.Requirements, as: SkillRequirements require OK @@ -293,147 +295,28 @@ defmodule Jido.Agent do @optional_callbacks [on_before_cmd: 2, on_after_cmd: 3, signal_routes: 0] - defmacro __using__(opts) do + # Helper functions that generate quoted code for the __using__ macro. + # This approach reduces the size of the main quote block to avoid + # "long quote blocks" and "nested too deep" Credo warnings. + + @doc false + def __quoted_module_setup__ do quote location: :keep do @behaviour Jido.Agent alias Jido.Agent + alias Jido.Agent.State, as: AgentState + alias Jido.Agent.Strategy, as: AgentStrategy alias Jido.Instruction + alias Jido.Skill.Requirements, as: SkillRequirements require OK + end + end - # Validate config at compile time - @validated_opts (case Zoi.parse(Agent.config_schema(), Map.new(unquote(opts))) do - {:ok, validated} -> - validated - - {:error, errors} -> - message = - "Invalid Agent configuration for #{inspect(__MODULE__)}: #{inspect(errors)}" - - raise CompileError, - description: message, - file: __ENV__.file, - line: __ENV__.line - end) - - # Normalize skills to Instance structs - @skill_instances Enum.map(@validated_opts[:skills] || [], fn skill_decl -> - # Extract module for validation first - mod = - case skill_decl do - m when is_atom(m) -> m - {m, _} -> m - end - - case Code.ensure_compiled(mod) do - {:module, _} -> - unless function_exported?(mod, :skill_spec, 1) do - raise CompileError, - description: - "#{inspect(mod)} does not implement Jido.Skill (missing skill_spec/1)", - file: __ENV__.file, - line: __ENV__.line - end - - {:error, reason} -> - raise CompileError, - description: - "Skill #{inspect(mod)} could not be compiled: #{inspect(reason)}", - file: __ENV__.file, - line: __ENV__.line - end - - Jido.Skill.Instance.new(skill_decl) - end) - - # Build skill specs from instances (for backward compatibility) - @skill_specs Enum.map(@skill_instances, fn instance -> - instance.module.skill_spec(instance.config) - |> Map.put(:state_key, instance.state_key) - end) - - # Validate unique state_keys (now derived from instances) - @skill_state_keys Enum.map(@skill_instances, & &1.state_key) - @duplicate_keys @skill_state_keys -- Enum.uniq(@skill_state_keys) - if @duplicate_keys != [] do - raise CompileError, - description: "Duplicate skill state_keys: #{inspect(@duplicate_keys)}", - file: __ENV__.file, - line: __ENV__.line - end - - # Validate no collision with base schema keys - @base_schema_keys Jido.Agent.Schema.known_keys(@validated_opts[:schema]) - @colliding_keys Enum.filter(@skill_state_keys, &(&1 in @base_schema_keys)) - if @colliding_keys != [] do - raise CompileError, - description: "Skill state_keys collide with agent schema: #{inspect(@colliding_keys)}", - file: __ENV__.file, - line: __ENV__.line - end - - # Merge schemas: base schema + nested skill schemas - @merged_schema Jido.Agent.Schema.merge_with_skills( - @validated_opts[:schema], - @skill_specs - ) - - # Aggregate actions from skills - @skill_actions @skill_specs |> Enum.flat_map(& &1.actions) |> Enum.uniq() - - # Expand routes from all skill instances - @expanded_skill_routes Enum.flat_map(@skill_instances, &Jido.Skill.Routes.expand_routes/1) - - # Expand schedules from all skill instances - @expanded_skill_schedules Enum.flat_map( - @skill_instances, - &Jido.Skill.Schedules.expand_schedules/1 - ) - - # Generate routes for schedule signal types (low priority) - @schedule_routes Enum.flat_map(@skill_instances, &Jido.Skill.Schedules.schedule_routes/1) - - # Combine routes and schedule routes for conflict detection - @all_skill_routes @expanded_skill_routes ++ @schedule_routes - - @skill_routes_result Jido.Skill.Routes.detect_conflicts(@all_skill_routes) - case @skill_routes_result do - {:error, conflicts} -> - conflict_list = Enum.join(conflicts, "\n - ") - - raise CompileError, - description: "Route conflicts detected:\n - #{conflict_list}", - file: __ENV__.file, - line: __ENV__.line - - {:ok, _routes} -> - :ok - end - - @validated_skill_routes elem(@skill_routes_result, 1) - - # Validate skill requirements at compile time - @skill_config_map Enum.reduce(@skill_instances, %{}, fn instance, acc -> - Map.put(acc, instance.state_key, instance.config) - end) - @requirements_result Jido.Skill.Requirements.validate_all_requirements( - @skill_instances, - @skill_config_map - ) - case @requirements_result do - {:error, missing_by_skill} -> - error_msg = Jido.Skill.Requirements.format_error(missing_by_skill) - - raise CompileError, - description: error_msg, - file: __ENV__.file, - line: __ENV__.line - - {:ok, :valid} -> - :ok - end - + @doc false + def __quoted_basic_accessors__ do + quote location: :keep do @doc "Returns the agent's name." @spec name() :: String.t() def name, do: @validated_opts.name @@ -457,7 +340,22 @@ defmodule Jido.Agent do @doc "Returns the merged schema (base + skill schemas)." @spec schema() :: Zoi.schema() | keyword() def schema, do: @merged_schema + end + end + @doc false + def __quoted_skill_accessors__ do + basic_skill_accessors = __quoted_basic_skill_accessors__() + computed_skill_accessors = __quoted_computed_skill_accessors__() + + quote location: :keep do + unquote(basic_skill_accessors) + unquote(computed_skill_accessors) + end + end + + defp __quoted_basic_skill_accessors__ do + quote location: :keep do @doc """ Returns the list of skill modules attached to this agent (deduplicated). @@ -487,7 +385,11 @@ defmodule Jido.Agent do @doc "Returns the list of actions from all attached skills." @spec actions() :: [module()] def actions, do: @skill_actions + end + end + defp __quoted_computed_skill_accessors__ do + quote location: :keep do @doc """ Returns the union of all capabilities from all mounted skill instances. @@ -529,7 +431,26 @@ defmodule Jido.Agent do @doc "Returns the expanded skill schedules." @spec skill_schedules() :: [Jido.Skill.Schedules.schedule_spec()] def skill_schedules, do: @expanded_skill_schedules + end + end + + @doc false + def __quoted_skill_config_accessors__ do + skill_config_public = __quoted_skill_config_public__() + skill_config_helpers = __quoted_skill_config_helpers__() + skill_state_public = __quoted_skill_state_public__() + skill_state_helpers = __quoted_skill_state_helpers__() + quote location: :keep do + unquote(skill_config_public) + unquote(skill_config_helpers) + unquote(skill_state_public) + unquote(skill_state_helpers) + end + end + + defp __quoted_skill_config_public__ do + quote location: :keep do @doc """ Returns the configuration for a specific skill. @@ -537,16 +458,7 @@ defmodule Jido.Agent do """ @spec skill_config(module() | {module(), atom()}) :: map() | nil def skill_config(skill_mod) when is_atom(skill_mod) do - case Enum.find(@skill_instances, &(&1.module == skill_mod and is_nil(&1.as))) do - nil -> - case Enum.find(@skill_instances, &(&1.module == skill_mod)) do - nil -> nil - instance -> instance.config - end - - instance -> - instance.config - end + __find_skill_config_by_module__(skill_mod) end def skill_config({skill_mod, as_alias}) when is_atom(skill_mod) and is_atom(as_alias) do @@ -555,7 +467,29 @@ defmodule Jido.Agent do instance -> instance.config end end + end + end + defp __quoted_skill_config_helpers__ do + quote location: :keep do + defp __find_skill_config_by_module__(skill_mod) do + case Enum.find(@skill_instances, &(&1.module == skill_mod and is_nil(&1.as))) do + nil -> __find_skill_config_fallback__(skill_mod) + instance -> instance.config + end + end + + defp __find_skill_config_fallback__(skill_mod) do + case Enum.find(@skill_instances, &(&1.module == skill_mod)) do + nil -> nil + instance -> instance.config + end + end + end + end + + defp __quoted_skill_state_public__ do + quote location: :keep do @doc """ Returns the state slice for a specific skill. @@ -563,16 +497,7 @@ defmodule Jido.Agent do """ @spec skill_state(Agent.t(), module() | {module(), atom()}) :: map() | nil def skill_state(agent, skill_mod) when is_atom(skill_mod) do - case Enum.find(@skill_instances, &(&1.module == skill_mod and is_nil(&1.as))) do - nil -> - case Enum.find(@skill_instances, &(&1.module == skill_mod)) do - nil -> nil - instance -> Map.get(agent.state, instance.state_key) - end - - instance -> - Map.get(agent.state, instance.state_key) - end + __find_skill_state_by_module__(agent, skill_mod) end def skill_state(agent, {skill_mod, as_alias}) @@ -582,7 +507,30 @@ defmodule Jido.Agent do instance -> Map.get(agent.state, instance.state_key) end end + end + end + defp __quoted_skill_state_helpers__ do + quote location: :keep do + defp __find_skill_state_by_module__(agent, skill_mod) do + case Enum.find(@skill_instances, &(&1.module == skill_mod and is_nil(&1.as))) do + nil -> __find_skill_state_fallback__(agent, skill_mod) + instance -> Map.get(agent.state, instance.state_key) + end + end + + defp __find_skill_state_fallback__(agent, skill_mod) do + case Enum.find(@skill_instances, &(&1.module == skill_mod)) do + nil -> nil + instance -> Map.get(agent.state, instance.state_key) + end + end + end + end + + @doc false + def __quoted_strategy_accessors__ do + quote location: :keep do @doc "Returns the execution strategy module for this agent." @spec strategy() :: module() def strategy do @@ -600,7 +548,22 @@ defmodule Jido.Agent do _ -> [] end end + end + end + @doc false + def __quoted_new_function__ do + new_fn = __quoted_new_fn_definition__() + mount_skills_fn = __quoted_mount_skills_definition__() + + quote location: :keep do + unquote(new_fn) + unquote(mount_skills_fn) + end + end + + defp __quoted_new_fn_definition__ do + quote location: :keep do @doc """ Creates a new agent with optional initial state. @@ -618,22 +581,7 @@ defmodule Jido.Agent do def new(opts \\ []) do opts = if is_list(opts), do: Map.new(opts), else: opts - # Build initial state from base schema defaults - base_defaults = Jido.Agent.State.defaults_from_schema(@validated_opts[:schema]) - - # Build skill defaults nested under their state_keys - skill_defaults = - @skill_specs - |> Enum.map(fn spec -> - skill_state_defaults = Jido.Agent.Schema.defaults_from_zoi_schema(spec.schema) - {spec.state_key, skill_state_defaults} - end) - |> Map.new() - - # Merge: base defaults + skill defaults + provided state - schema_defaults = Map.merge(base_defaults, skill_defaults) - initial_state = Map.merge(schema_defaults, opts[:state] || %{}) - + initial_state = __build_initial_state__(opts) id = opts[:id] || Jido.Util.generate_id() agent = %Agent{ @@ -648,28 +596,7 @@ defmodule Jido.Agent do } # Run skill mount hooks (pure initialization) - agent = - Enum.reduce(@skill_specs, agent, fn spec, agent_acc -> - mod = spec.module - config = spec.config || %{} - - case mod.mount(agent_acc, config) do - {:ok, skill_state} when is_map(skill_state) -> - current_skill_state = Map.get(agent_acc.state, spec.state_key, %{}) - merged_skill_state = Map.merge(current_skill_state, skill_state) - new_state = Map.put(agent_acc.state, spec.state_key, merged_skill_state) - %{agent_acc | state: new_state} - - {:ok, nil} -> - agent_acc - - {:error, reason} -> - raise Jido.Error.internal_error( - "Skill mount failed for #{inspect(mod)}", - %{skill: mod, reason: reason} - ) - end - end) + agent = __mount_skills__(agent) # Run strategy initialization (directives are dropped here; # AgentServer handles init directives separately) @@ -678,6 +605,61 @@ defmodule Jido.Agent do initialized_agent end + defp __build_initial_state__(opts) do + # Build initial state from base schema defaults + base_defaults = AgentState.defaults_from_schema(@validated_opts[:schema]) + + # Build skill defaults nested under their state_keys + skill_defaults = + @skill_specs + |> Enum.map(fn spec -> + skill_state_defaults = Jido.Agent.Schema.defaults_from_zoi_schema(spec.schema) + {spec.state_key, skill_state_defaults} + end) + |> Map.new() + + # Merge: base defaults + skill defaults + provided state + schema_defaults = Map.merge(base_defaults, skill_defaults) + Map.merge(schema_defaults, opts[:state] || %{}) + end + end + end + + defp __quoted_mount_skills_definition__ do + quote location: :keep do + defp __mount_skills__(agent) do + Enum.reduce(@skill_specs, agent, fn spec, agent_acc -> + __mount_single_skill__(agent_acc, spec) + end) + end + + defp __mount_single_skill__(agent_acc, spec) do + mod = spec.module + config = spec.config || %{} + + case mod.mount(agent_acc, config) do + {:ok, skill_state} when is_map(skill_state) -> + current_skill_state = Map.get(agent_acc.state, spec.state_key, %{}) + merged_skill_state = Map.merge(current_skill_state, skill_state) + new_state = Map.put(agent_acc.state, spec.state_key, merged_skill_state) + %{agent_acc | state: new_state} + + {:ok, nil} -> + agent_acc + + {:error, reason} -> + raise Jido.Error.internal_error( + "Skill mount failed for #{inspect(mod)}", + %{skill: mod, reason: reason} + ) + end + end + end + end + + @doc false + def __quoted_cmd_function__ do + quote location: :keep do @doc """ Execute actions against the agent. Pure: `(agent, action) -> {agent, directives}` @@ -708,18 +690,23 @@ defmodule Jido.Agent do normalized_instructions = Enum.map(instructions, fn instr -> - Jido.Agent.Strategy.normalize_instruction(strat, instr, ctx) + AgentStrategy.normalize_instruction(strat, instr, ctx) end) {agent, directives} = strat.cmd(agent, normalized_instructions, ctx) - do_after_cmd(agent, action, directives) + __do_after_cmd__(agent, action, directives) {:error, reason} -> error = Jido.Error.validation_error("Invalid action", %{reason: reason}) - {agent, [%Directive.Error{error: error, context: :normalize}]} + {agent, [%Jido.Agent.Directive.Error{error: error, context: :normalize}]} end end + end + end + @doc false + def __quoted_utility_functions__ do + quote location: :keep do @doc """ Returns a stable, public view of the strategy's execution state. @@ -748,7 +735,7 @@ defmodule Jido.Agent do """ @spec set(Agent.t(), map() | keyword()) :: Agent.agent_result() def set(%Agent{} = agent, attrs) do - new_state = Jido.Agent.State.merge(agent.state, Map.new(attrs)) + new_state = AgentState.merge(agent.state, Map.new(attrs)) OK.success(%{agent | state: new_state}) end @@ -765,7 +752,7 @@ defmodule Jido.Agent do """ @spec validate(Agent.t(), keyword()) :: Agent.agent_result() def validate(%Agent{} = agent, opts \\ []) do - case Jido.Agent.State.validate(agent.state, agent.schema, opts) do + case AgentState.validate(agent.state, agent.schema, opts) do {:ok, validated_state} -> OK.success(%{agent | state: validated_state}) @@ -774,7 +761,12 @@ defmodule Jido.Agent do |> OK.failure() end end + end + end + @doc false + def __quoted_callbacks__ do + quote location: :keep do # Default callback implementations @spec on_before_cmd(Agent.t(), Agent.action()) :: {:ok, Agent.t(), Agent.action()} @@ -810,13 +802,183 @@ defmodule Jido.Agent do skill_schedules: 0 # Private helper for after hook dispatch - defp do_after_cmd(agent, msg, directives) do + defp __do_after_cmd__(agent, msg, directives) do {:ok, agent, directives} = on_after_cmd(agent, msg, directives) {agent, directives} end end end + defmacro __using__(opts) do + # Get the quoted blocks from helper functions + module_setup = Agent.__quoted_module_setup__() + basic_accessors = Agent.__quoted_basic_accessors__() + skill_accessors = Agent.__quoted_skill_accessors__() + skill_config_accessors = Agent.__quoted_skill_config_accessors__() + strategy_accessors = Agent.__quoted_strategy_accessors__() + new_function = Agent.__quoted_new_function__() + cmd_function = Agent.__quoted_cmd_function__() + utility_functions = Agent.__quoted_utility_functions__() + callbacks = Agent.__quoted_callbacks__() + + # Build compile-time validation and module attributes as a separate smaller block + compile_time_setup = + quote location: :keep do + # Validate config at compile time + @validated_opts (case Zoi.parse(Agent.config_schema(), Map.new(unquote(opts))) do + {:ok, validated} -> + validated + + {:error, errors} -> + message = + "Invalid Agent configuration for #{inspect(__MODULE__)}: #{inspect(errors)}" + + raise CompileError, + description: message, + file: __ENV__.file, + line: __ENV__.line + end) + + # Normalize skills to Instance structs + @skill_instances Jido.Agent.__normalize_skill_instances__(@validated_opts[:skills] || []) + + # Build skill specs from instances (for backward compatibility) + @skill_specs Enum.map(@skill_instances, fn instance -> + instance.module.skill_spec(instance.config) + |> Map.put(:state_key, instance.state_key) + end) + + # Validate unique state_keys (now derived from instances) + @skill_state_keys Enum.map(@skill_instances, & &1.state_key) + @duplicate_keys @skill_state_keys -- Enum.uniq(@skill_state_keys) + if @duplicate_keys != [] do + raise CompileError, + description: "Duplicate skill state_keys: #{inspect(@duplicate_keys)}", + file: __ENV__.file, + line: __ENV__.line + end + + # Validate no collision with base schema keys + @base_schema_keys Jido.Agent.Schema.known_keys(@validated_opts[:schema]) + @colliding_keys Enum.filter(@skill_state_keys, &(&1 in @base_schema_keys)) + if @colliding_keys != [] do + raise CompileError, + description: + "Skill state_keys collide with agent schema: #{inspect(@colliding_keys)}", + file: __ENV__.file, + line: __ENV__.line + end + + # Merge schemas: base schema + nested skill schemas + @merged_schema Jido.Agent.Schema.merge_with_skills( + @validated_opts[:schema], + @skill_specs + ) + + # Aggregate actions from skills + @skill_actions @skill_specs |> Enum.flat_map(& &1.actions) |> Enum.uniq() + + # Expand routes from all skill instances + @expanded_skill_routes Enum.flat_map(@skill_instances, &Jido.Skill.Routes.expand_routes/1) + + # Expand schedules from all skill instances + @expanded_skill_schedules Enum.flat_map( + @skill_instances, + &Jido.Skill.Schedules.expand_schedules/1 + ) + + # Generate routes for schedule signal types (low priority) + @schedule_routes Enum.flat_map(@skill_instances, &Jido.Skill.Schedules.schedule_routes/1) + + # Combine routes and schedule routes for conflict detection + @all_skill_routes @expanded_skill_routes ++ @schedule_routes + + @skill_routes_result Jido.Skill.Routes.detect_conflicts(@all_skill_routes) + case @skill_routes_result do + {:error, conflicts} -> + conflict_list = Enum.join(conflicts, "\n - ") + + raise CompileError, + description: "Route conflicts detected:\n - #{conflict_list}", + file: __ENV__.file, + line: __ENV__.line + + {:ok, _routes} -> + :ok + end + + @validated_skill_routes elem(@skill_routes_result, 1) + + # Validate skill requirements at compile time + @skill_config_map Enum.reduce(@skill_instances, %{}, fn instance, acc -> + Map.put(acc, instance.state_key, instance.config) + end) + @requirements_result Jido.Skill.Requirements.validate_all_requirements( + @skill_instances, + @skill_config_map + ) + case @requirements_result do + {:error, missing_by_skill} -> + error_msg = SkillRequirements.format_error(missing_by_skill) + + raise CompileError, + description: error_msg, + file: __ENV__.file, + line: __ENV__.line + + {:ok, :valid} -> + :ok + end + end + + # Combine all blocks using unquote + quote location: :keep do + unquote(module_setup) + unquote(compile_time_setup) + unquote(basic_accessors) + unquote(skill_accessors) + unquote(skill_config_accessors) + unquote(strategy_accessors) + unquote(new_function) + unquote(cmd_function) + unquote(utility_functions) + unquote(callbacks) + end + end + + @doc false + def __normalize_skill_instances__(skills) do + Enum.map(skills, &__validate_and_create_skill_instance__/1) + end + + defp __validate_and_create_skill_instance__(skill_decl) do + mod = __extract_skill_module__(skill_decl) + __validate_skill_module__(mod) + SkillInstance.new(skill_decl) + end + + defp __extract_skill_module__(m) when is_atom(m), do: m + defp __extract_skill_module__({m, _}), do: m + + defp __validate_skill_module__(mod) do + case Code.ensure_compiled(mod) do + {:module, _} -> __validate_skill_behaviour__(mod) + {:error, reason} -> __raise_skill_compile_error__(mod, reason) + end + end + + defp __validate_skill_behaviour__(mod) do + unless function_exported?(mod, :skill_spec, 1) do + raise CompileError, + description: "#{inspect(mod)} does not implement Jido.Skill (missing skill_spec/1)" + end + end + + defp __raise_skill_compile_error__(mod, reason) do + raise CompileError, + description: "Skill #{inspect(mod)} could not be compiled: #{inspect(reason)}" + end + # Base module functions (for direct use without `use`) @doc """ diff --git a/lib/jido/agent/directive/cron.ex b/lib/jido/agent/directive/cron.ex index 5bac5c4..7099699 100644 --- a/lib/jido/agent/directive/cron.ex +++ b/lib/jido/agent/directive/cron.ex @@ -62,54 +62,57 @@ defimpl Jido.AgentServer.DirectiveExec, for: Jido.Agent.Directive.Cron do state ) do agent_id = state.id - logical_id = logical_id || make_ref() + signal = build_signal(message, logical_id, agent_id) - signal = - case message do - %Jido.Signal{} = s -> - s + cancel_existing_job(state.cron_jobs, logical_id) - other -> - CronTick.new!( - %{job_id: logical_id, message: other}, - source: "/agent/#{agent_id}" - ) - end + opts = build_scheduler_opts(tz) + + Jido.Scheduler.run_every( + fn -> + _ = Jido.AgentServer.cast(agent_id, signal) + :ok + end, + cron_expr, + opts + ) + |> handle_scheduler_result(state, agent_id, logical_id, cron_expr) + end + + defp build_signal(%Jido.Signal{} = signal, _logical_id, _agent_id), do: signal + + defp build_signal(message, logical_id, agent_id) do + CronTick.new!( + %{job_id: logical_id, message: message}, + source: "/agent/#{agent_id}" + ) + end - case Map.get(state.cron_jobs, logical_id) do - nil -> :ok - existing_pid when is_pid(existing_pid) -> Jido.Scheduler.cancel(existing_pid) + defp cancel_existing_job(cron_jobs, logical_id) do + case Map.get(cron_jobs, logical_id) do + pid when is_pid(pid) -> Jido.Scheduler.cancel(pid) _ -> :ok end + end - opts = if tz, do: [timezone: tz], else: [] - - result = - Jido.Scheduler.run_every( - fn -> - _ = Jido.AgentServer.cast(agent_id, signal) - :ok - end, - cron_expr, - opts - ) - - case result do - {:ok, pid} -> - Logger.debug( - "AgentServer #{agent_id} registered cron job #{inspect(logical_id)}: #{cron_expr}" - ) - - new_state = put_in(state.cron_jobs[logical_id], pid) - {:ok, new_state} - - {:error, reason} -> - Logger.error( - "AgentServer #{agent_id} failed to register cron job #{inspect(logical_id)}: #{inspect(reason)}" - ) - - {:error, reason} - end + defp build_scheduler_opts(nil), do: [] + defp build_scheduler_opts(tz), do: [timezone: tz] + + defp handle_scheduler_result({:ok, pid}, state, agent_id, logical_id, cron_expr) do + Logger.debug( + "AgentServer #{agent_id} registered cron job #{inspect(logical_id)}: #{cron_expr}" + ) + + new_state = put_in(state.cron_jobs[logical_id], pid) + {:ok, new_state} + end + + defp handle_scheduler_result({:error, reason}, _state, agent_id, logical_id, _cron_expr) do + Logger.error( + "AgentServer #{agent_id} failed to register cron job #{inspect(logical_id)}: #{inspect(reason)}" + ) + + {:error, reason} end end diff --git a/lib/jido/agent/instance_manager.ex b/lib/jido/agent/instance_manager.ex index 5014d41..f8abf1b 100644 --- a/lib/jido/agent/instance_manager.ex +++ b/lib/jido/agent/instance_manager.ex @@ -73,6 +73,8 @@ defmodule Jido.Agent.InstanceManager do require Logger + alias Jido.Agent.Persistence + @type manager_name :: atom() @type key :: term() @@ -275,6 +277,8 @@ defmodule Jido.Agent.InstanceManager do base_opts = [ agent: agent_or_nil || config.agent, + # When thawing from persistence we pass a struct, so keep the module explicit. + agent_module: config.agent, id: key_to_id(key), name: {:via, Registry, {registry_name(config.name), key}}, # Instance manager lifecycle options @@ -293,13 +297,14 @@ defmodule Jido.Agent.InstanceManager do Keyword.put(base_opts, :initial_state, initial_state) end - {Jido.AgentServer, base_opts} + # Avoid immediate restarts on normal shutdown/idle timeout; allow restarts on crashes. + Supervisor.child_spec({Jido.AgentServer, base_opts}, restart: :transient) end defp maybe_thaw(%{persistence: nil}, _key), do: nil defp maybe_thaw(%{persistence: persistence, agent: agent_module}, key) do - case Jido.Agent.Persistence.thaw(persistence, agent_module, key) do + case Persistence.thaw(persistence, agent_module, key) do {:ok, agent} -> Logger.debug("InstanceManager thawed agent for key #{inspect(key)}") agent diff --git a/lib/jido/agent/state_ops.ex b/lib/jido/agent/state_ops.ex index 885d41d..8d17ac4 100644 --- a/lib/jido/agent/state_ops.ex +++ b/lib/jido/agent/state_ops.ex @@ -17,6 +17,7 @@ defmodule Jido.Agent.StateOps do """ alias Jido.Agent + alias Jido.Agent.State alias Jido.Agent.StateOp @doc """ @@ -26,7 +27,7 @@ defmodule Jido.Agent.StateOps do """ @spec apply_result(Agent.t(), map()) :: Agent.t() def apply_result(%Agent{} = agent, result) when is_map(result) do - new_state = Jido.Agent.State.merge(agent.state, result) + new_state = State.merge(agent.state, result) %{agent | state: new_state} end @@ -42,7 +43,7 @@ defmodule Jido.Agent.StateOps do def apply_state_ops(%Agent{} = agent, effects) do Enum.reduce(effects, {agent, []}, fn %StateOp.SetState{attrs: attrs}, {a, directives} -> - new_state = Jido.Agent.State.merge(a.state, attrs) + new_state = State.merge(a.state, attrs) {%{a | state: new_state}, directives} %StateOp.ReplaceState{state: new_state}, {a, directives} -> diff --git a/lib/jido/agent/strategy.ex b/lib/jido/agent/strategy.ex index 91eb8eb..11d50ca 100644 --- a/lib/jido/agent/strategy.ex +++ b/lib/jido/agent/strategy.ex @@ -76,6 +76,7 @@ defmodule Jido.Agent.Strategy do `:__strategy__`. Use `Jido.Agent.Strategy.State` helpers to manage it. """ + alias Jido.Action.Tool, as: ActionTool alias Jido.Agent alias Jido.Agent.Strategy.State, as: StratState @@ -354,7 +355,7 @@ defmodule Jido.Agent.Strategy do end is_list(schema) -> - Jido.Action.Tool.convert_params_using_schema(params, schema) + ActionTool.convert_params_using_schema(params, schema) true -> atomized @@ -377,11 +378,9 @@ defmodule Jido.Agent.Strategy do defp atomize_string_keys(other), do: other defp safe_to_atom(str) when is_binary(str) do - try do - String.to_existing_atom(str) - rescue - ArgumentError -> String.to_atom(str) - end + String.to_existing_atom(str) + rescue + ArgumentError -> String.to_atom(str) end defmacro __using__(_opts) do @@ -401,7 +400,7 @@ defmodule Jido.Agent.Strategy do @impl true @spec snapshot(Jido.Agent.t(), Jido.Agent.Strategy.context()) :: Jido.Agent.Strategy.Snapshot.t() - def snapshot(agent, _ctx), do: Jido.Agent.Strategy.default_snapshot(agent) + def snapshot(agent, _ctx), do: unquote(__MODULE__).default_snapshot(agent) defoverridable init: 2, tick: 2, snapshot: 2 end diff --git a/lib/jido/agent/strategy/fsm.ex b/lib/jido/agent/strategy/fsm.ex index 1f8ffe4..74fe463 100644 --- a/lib/jido/agent/strategy/fsm.ex +++ b/lib/jido/agent/strategy/fsm.ex @@ -155,15 +155,7 @@ defmodule Jido.Agent.Strategy.FSM do {:ok, machine} -> {agent, machine, directives} = process_instructions(agent, machine, instructions) - machine = - if auto_transition do - case Machine.transition(machine, initial_state) do - {:ok, m} -> m - {:error, _} -> machine - end - else - machine - end + machine = maybe_auto_transition(machine, auto_transition, initial_state) agent = StratState.put(agent, %{state | machine: machine}) {agent, directives} @@ -184,6 +176,15 @@ defmodule Jido.Agent.Strategy.FSM do end) end + defp maybe_auto_transition(machine, false, _initial_state), do: machine + + defp maybe_auto_transition(machine, true, initial_state) do + case Machine.transition(machine, initial_state) do + {:ok, m} -> m + {:error, _} -> machine + end + end + defp run_instruction(agent, machine, %Instruction{} = instruction) do instruction = %{instruction | context: Map.put(instruction.context, :state, agent.state)} diff --git a/lib/jido/agent_server.ex b/lib/jido/agent_server.ex index c159b4e..ba706d0 100644 --- a/lib/jido/agent_server.ex +++ b/lib/jido/agent_server.ex @@ -122,12 +122,13 @@ defmodule Jido.AgentServer do Status } - alias Jido.AgentServer.Signal.{ChildExit, ChildStarted, Orphaned} alias Jido.Agent.Directive + alias Jido.AgentServer.Signal.{ChildExit, ChildStarted, Orphaned} + alias Jido.Sensor.Runtime, as: SensorRuntime alias Jido.Signal - alias Jido.Tracing.Trace - alias Jido.Tracing.Context, as: TraceContext alias Jido.Signal.Router, as: JidoRouter + alias Jido.Tracing.Context, as: TraceContext + alias Jido.Tracing.Trace @type server :: pid() | atom() | {:via, module(), term()} | String.t() @@ -753,21 +754,25 @@ defmodule Jido.AgentServer do state = %{state | completion_waiters: new_waiters} - cond do - match?(%{parent: %ParentRef{pid: ^pid}}, state) -> - handle_parent_down(state, pid, reason) - - true -> - handle_child_down(state, pid, reason) + if match?(%{parent: %ParentRef{pid: ^pid}}, state) do + handle_parent_down(state, pid, reason) + else + handle_child_down(state, pid, reason) end end end - def handle_info(:lifecycle_idle_timeout, state) do - # Delegate to lifecycle module - case state.lifecycle.mod.handle_event(:idle_timeout, state) do - {:cont, state} -> {:noreply, state} - {:stop, reason, state} -> {:stop, reason, state} + def handle_info({:timeout, ref, :lifecycle_idle_timeout}, state) do + if state.lifecycle.idle_timer == ref do + # Clear the timer so stale messages don't trigger after cancel/reset. + state = %{state | lifecycle: %{state.lifecycle | idle_timer: nil}} + + case state.lifecycle.mod.handle_event(:idle_timeout, state) do + {:cont, state} -> {:noreply, state} + {:stop, reason, state} -> {:stop, reason, state} + end + else + {:noreply, state} end end @@ -804,17 +809,7 @@ defmodule Jido.AgentServer do defp process_signal(%Signal{} = signal, %State{signal_router: router} = state) do start_time = System.monotonic_time() - agent_module = state.agent_module - - trace_metadata = TraceContext.to_telemetry_metadata() - - metadata = - %{ - agent_id: state.id, - agent_module: agent_module, - signal_type: signal.type - } - |> Map.merge(trace_metadata) + metadata = build_signal_metadata(state, signal) emit_telemetry( [:jido, :agent_server, :signal, :start], @@ -823,47 +818,7 @@ defmodule Jido.AgentServer do ) try do - case run_skill_signal_hooks(signal, state) do - {:error, error} -> - error_directive = %Directive.Error{error: error, context: :skill_handle_signal} - - case State.enqueue_all(state, signal, [error_directive]) do - {:ok, enq_state} -> {:error, error, start_drain_if_idle(enq_state)} - {:error, :queue_overflow} -> {:error, error, state} - end - - {:override, action_spec} -> - dispatch_action(signal, action_spec, state, start_time, metadata) - - :continue -> - case route_to_actions(router, signal) do - {:ok, actions} -> - dispatch_action(signal, actions, state, start_time, metadata) - - {:error, reason} -> - emit_telemetry( - [:jido, :agent_server, :signal, :stop], - %{duration: System.monotonic_time() - start_time}, - Map.merge(metadata, %{error: reason}) - ) - - error = - Jido.Error.routing_error("No route for signal", %{ - signal_type: signal.type, - reason: reason - }) - - error_directive = %Directive.Error{error: error, context: :routing} - - case State.enqueue_all(state, signal, [error_directive]) do - {:ok, enq_state} -> - {:error, reason, start_drain_if_idle(enq_state)} - - {:error, :queue_overflow} -> - {:error, reason, state} - end - end - end + do_process_signal(signal, router, state, start_time, metadata) catch kind, reason -> emit_telemetry( @@ -876,6 +831,69 @@ defmodule Jido.AgentServer do end end + defp build_signal_metadata(state, signal) do + trace_metadata = TraceContext.to_telemetry_metadata() + + %{ + agent_id: state.id, + agent_module: state.agent_module, + signal_type: signal.type + } + |> Map.merge(trace_metadata) + end + + defp do_process_signal(signal, router, state, start_time, metadata) do + case run_skill_signal_hooks(signal, state) do + {:error, error} -> + handle_skill_hook_error(error, signal, state) + + {:override, action_spec} -> + dispatch_action(signal, action_spec, state, start_time, metadata) + + :continue -> + handle_signal_routing(signal, router, state, start_time, metadata) + end + end + + defp handle_skill_hook_error(error, signal, state) do + error_directive = %Directive.Error{error: error, context: :skill_handle_signal} + enqueue_error_directive(error, signal, [error_directive], state) + end + + defp handle_signal_routing(signal, router, state, start_time, metadata) do + case route_to_actions(router, signal) do + {:ok, actions} -> + dispatch_action(signal, actions, state, start_time, metadata) + + {:error, reason} -> + handle_routing_error(reason, signal, state, start_time, metadata) + end + end + + defp handle_routing_error(reason, signal, state, start_time, metadata) do + emit_telemetry( + [:jido, :agent_server, :signal, :stop], + %{duration: System.monotonic_time() - start_time}, + Map.merge(metadata, %{error: reason}) + ) + + error = + Jido.Error.routing_error("No route for signal", %{ + signal_type: signal.type, + reason: reason + }) + + error_directive = %Directive.Error{error: error, context: :routing} + enqueue_error_directive(reason, signal, [error_directive], state) + end + + defp enqueue_error_directive(error, signal, directives, state) do + case State.enqueue_all(state, signal, directives) do + {:ok, enq_state} -> {:error, error, start_drain_if_idle(enq_state)} + {:error, :queue_overflow} -> {:error, error, state} + end + end + defp dispatch_action(signal, action_spec, state, start_time, metadata) do agent_module = state.agent_module @@ -970,34 +988,38 @@ defmodule Jido.AgentServer do {:halt, acc} :continue -> - context = %{ - agent: state.agent, - agent_module: agent_module, - skill: spec.module, - skill_spec: spec, - config: spec.config || %{} - } - - case spec.module.handle_signal(signal, context) do - {:ok, {:override, action_spec}} -> - {:halt, {:override, action_spec}} - - {:ok, _} -> - {:cont, :continue} - - {:error, reason} -> - error = - Jido.Error.execution_error( - "Skill handle_signal failed", - %{skill: spec.module, reason: reason} - ) - - {:halt, {:error, error}} - end + invoke_skill_handle_signal(spec, signal, state, agent_module) end end) end + defp invoke_skill_handle_signal(spec, signal, state, agent_module) do + context = %{ + agent: state.agent, + agent_module: agent_module, + skill: spec.module, + skill_spec: spec, + config: spec.config || %{} + } + + case spec.module.handle_signal(signal, context) do + {:ok, {:override, action_spec}} -> + {:halt, {:override, action_spec}} + + {:ok, _} -> + {:cont, :continue} + + {:error, reason} -> + error = + Jido.Error.execution_error( + "Skill handle_signal failed", + %{skill: spec.module, reason: reason} + ) + + {:halt, {:error, error}} + end + end + # --------------------------------------------------------------------------- # Internal: Skill Transform Hooks # --------------------------------------------------------------------------- @@ -1038,27 +1060,30 @@ defmodule Jido.AgentServer do Enum.reduce(skill_specs, state, fn spec, acc_state -> config = spec.config || %{} + start_skill_spec_children(acc_state, spec.module, config) + end) + end - case spec.module.child_spec(config) do - nil -> - acc_state + defp start_skill_spec_children(state, skill_module, config) do + case skill_module.child_spec(config) do + nil -> + state - %{} = child_spec -> - start_skill_child(acc_state, spec.module, child_spec) + %{} = child_spec -> + start_skill_child(state, skill_module, child_spec) - list when is_list(list) -> - Enum.reduce(list, acc_state, fn cs, s -> - start_skill_child(s, spec.module, cs) - end) + list when is_list(list) -> + Enum.reduce(list, state, fn cs, s -> + start_skill_child(s, skill_module, cs) + end) - other -> - Logger.warning( - "Invalid child_spec from skill #{inspect(spec.module)}: #{inspect(other)}" - ) + other -> + Logger.warning( + "Invalid child_spec from skill #{inspect(skill_module)}: #{inspect(other)}" + ) - acc_state - end - end) + state + end end defp start_skill_child(%State{} = state, skill_module, %{start: {m, f, a}} = spec) do @@ -1143,7 +1168,7 @@ defmodule Jido.AgentServer do context: context ] - case Jido.Sensor.Runtime.start_link(opts) do + case SensorRuntime.start_link(opts) do {:ok, pid} -> ref = Process.monitor(pid) tag = {:sensor, skill_module, sensor_module} diff --git a/lib/jido/agent_server/directive_executors.ex b/lib/jido/agent_server/directive_executors.ex index d882fa1..52dd19d 100644 --- a/lib/jido/agent_server/directive_executors.ex +++ b/lib/jido/agent_server/directive_executors.ex @@ -14,25 +14,27 @@ defimpl Jido.AgentServer.DirectiveExec, for: Jido.Agent.Directive.Emit do {:error, _} -> signal end - case cfg do - nil -> - Logger.debug("Emit directive with no dispatch config, signal: #{traced_signal.type}") - - cfg -> - if Code.ensure_loaded?(Jido.Signal.Dispatch) do - task_sup = - if state.jido, do: Jido.task_supervisor_name(state.jido), else: Jido.TaskSupervisor - - Task.Supervisor.start_child(task_sup, fn -> - Jido.Signal.Dispatch.dispatch(traced_signal, cfg) - end) - else - Logger.warning("Jido.Signal.Dispatch not available, skipping emit") - end - end + dispatch_signal(traced_signal, cfg, state) {:async, nil, state} end + + defp dispatch_signal(traced_signal, nil, _state) do + Logger.debug("Emit directive with no dispatch config, signal: #{traced_signal.type}") + end + + defp dispatch_signal(traced_signal, cfg, state) do + if Code.ensure_loaded?(Jido.Signal.Dispatch) do + task_sup = + if state.jido, do: Jido.task_supervisor_name(state.jido), else: Jido.TaskSupervisor + + Task.Supervisor.start_child(task_sup, fn -> + Jido.Signal.Dispatch.dispatch(traced_signal, cfg) + end) + else + Logger.warning("Jido.Signal.Dispatch not available, skipping emit") + end + end end defimpl Jido.AgentServer.DirectiveExec, for: Jido.Agent.Directive.Error do @@ -52,15 +54,13 @@ defimpl Jido.AgentServer.DirectiveExec, for: Jido.Agent.Directive.Spawn do def exec(%{child_spec: child_spec, tag: tag}, _input_signal, state) do result = - cond do - is_function(state.spawn_fun, 1) -> - state.spawn_fun.(child_spec) - - true -> - agent_sup = - if state.jido, do: Jido.agent_supervisor_name(state.jido), else: Jido.AgentSupervisor + if is_function(state.spawn_fun, 1) do + state.spawn_fun.(child_spec) + else + agent_sup = + if state.jido, do: Jido.agent_supervisor_name(state.jido), else: Jido.AgentSupervisor - DynamicSupervisor.start_child(agent_sup, child_spec) + DynamicSupervisor.start_child(agent_sup, child_spec) end case result do diff --git a/lib/jido/agent_server/error_policy.ex b/lib/jido/agent_server/error_policy.ex index 07e9495..48fbdf5 100644 --- a/lib/jido/agent_server/error_policy.ex +++ b/lib/jido/agent_server/error_policy.ex @@ -13,6 +13,7 @@ defmodule Jido.AgentServer.ErrorPolicy do alias Jido.Agent.Directive.Error, as: ErrorDirective alias Jido.AgentServer.State + alias Jido.Signal.Dispatch, as: SignalDispatch @type result :: {:ok, State.t()} | {:stop, term(), State.t()} @@ -61,12 +62,12 @@ defmodule Jido.AgentServer.ErrorPolicy do defp emit_error_signal(error, context, state, dispatch_cfg) do signal = build_error_signal(error, context, state) - if Code.ensure_loaded?(Jido.Signal.Dispatch) do + if Code.ensure_loaded?(SignalDispatch) do task_supervisor = if state.jido, do: Jido.task_supervisor_name(state.jido), else: Jido.TaskSupervisor Task.Supervisor.start_child(task_supervisor, fn -> - Jido.Signal.Dispatch.dispatch(signal, dispatch_cfg) + SignalDispatch.dispatch(signal, dispatch_cfg) end) else Logger.warning("Jido.Signal.Dispatch not available, skipping error signal emit") diff --git a/lib/jido/agent_server/lifecycle/keyed.ex b/lib/jido/agent_server/lifecycle/keyed.ex index 793073c..c2c7407 100644 --- a/lib/jido/agent_server/lifecycle/keyed.ex +++ b/lib/jido/agent_server/lifecycle/keyed.ex @@ -31,6 +31,8 @@ defmodule Jido.AgentServer.Lifecycle.Keyed do require Logger + alias Jido.Agent.Persistence + @impl true def init(_opts, state) do # The lifecycle struct is already populated by State.from_options @@ -164,7 +166,7 @@ defmodule Jido.AgentServer.Lifecycle.Keyed do agent = state.agent agent_module = state.agent_module - case Jido.Agent.Persistence.hibernate(persistence, agent_module, pool_key, agent) do + case Persistence.hibernate(persistence, agent_module, pool_key, agent) do :ok -> Logger.debug("Lifecycle hibernated agent for #{lifecycle.pool}/#{inspect(pool_key)}") @@ -184,7 +186,8 @@ defmodule Jido.AgentServer.Lifecycle.Keyed do state MapSet.size(lifecycle.attachments) == 0 and is_integer(timeout) and timeout > 0 -> - timer_ref = Process.send_after(self(), :lifecycle_idle_timeout, timeout) + # Use a timer ref so stale timeout messages can be ignored safely. + timer_ref = :erlang.start_timer(timeout, self(), :lifecycle_idle_timeout) %{state | lifecycle: %{lifecycle | idle_timer: timer_ref}} true -> @@ -196,7 +199,7 @@ defmodule Jido.AgentServer.Lifecycle.Keyed do lifecycle = state.lifecycle if lifecycle.idle_timer do - Process.cancel_timer(lifecycle.idle_timer) + :erlang.cancel_timer(lifecycle.idle_timer) %{state | lifecycle: %{lifecycle | idle_timer: nil}} else state diff --git a/lib/jido/await.ex b/lib/jido/await.ex index 799a472..77b9273 100644 --- a/lib/jido/await.ex +++ b/lib/jido/await.ex @@ -126,16 +126,13 @@ defmodule Jido.Await do if now_ms() > deadline do {:error, :timeout} else - case AgentServer.state(parent_server) do - {:ok, %{children: children}} -> - case Map.get(children, child_tag) do - %{pid: pid} when is_pid(pid) -> - {:ok, pid} + case lookup_child_pid(parent_server, child_tag) do + {:ok, pid} -> + {:ok, pid} - _ -> - sleep(poll_interval) - poll_for_child(parent_server, child_tag, deadline, poll_interval) - end + {:error, :not_found} -> + sleep(poll_interval) + poll_for_child(parent_server, child_tag, deadline, poll_interval) {:error, _} = error -> error @@ -143,6 +140,19 @@ defmodule Jido.Await do end end + defp lookup_child_pid(parent_server, child_tag) do + case AgentServer.state(parent_server) do + {:ok, %{children: children}} -> + case Map.get(children, child_tag) do + %{pid: pid} when is_pid(pid) -> {:ok, pid} + _ -> {:error, :not_found} + end + + {:error, _} = error -> + error + end + end + # --------------------------------------------------------------------------- # Multi-Agent Coordination # --------------------------------------------------------------------------- diff --git a/lib/jido/igniter/templates.ex b/lib/jido/igniter/templates.ex index a99adfe..fd60600 100644 --- a/lib/jido/igniter/templates.ex +++ b/lib/jido/igniter/templates.ex @@ -53,10 +53,7 @@ defmodule Jido.Igniter.Templates do signal_patterns :: [String.t()] ) :: String.t() def skill_template(module, name, state_key, signal_patterns) do - patterns_str = - signal_patterns - |> Enum.map(&~s("#{&1}")) - |> Enum.join(", ") + patterns_str = Enum.map_join(signal_patterns, ", ", &~s("#{&1}")) """ defmodule #{module} do diff --git a/lib/jido/observe.ex b/lib/jido/observe.ex index aa00b6d..c04fddd 100644 --- a/lib/jido/observe.ex +++ b/lib/jido/observe.ex @@ -78,7 +78,9 @@ defmodule Jido.Observe do require Logger + alias Jido.Observe.Log alias Jido.Observe.SpanCtx + alias Jido.Tracing.Context, as: TracingContext @type event_prefix :: [atom()] @type metadata :: map() @@ -287,7 +289,7 @@ defmodule Jido.Observe do """ @spec log(Logger.level(), Logger.message(), keyword()) :: :ok def log(level, message, metadata \\ []) do - Jido.Observe.Log.log(level, message, metadata) + Log.log(level, message, metadata) end @doc """ @@ -409,8 +411,8 @@ defmodule Jido.Observe do end defp correlation_metadata do - if Code.ensure_loaded?(Jido.Tracing.Context) do - Jido.Tracing.Context.to_telemetry_metadata() + if Code.ensure_loaded?(TracingContext) do + TracingContext.to_telemetry_metadata() else %{} end diff --git a/lib/jido/sensor/runtime.ex b/lib/jido/sensor/runtime.ex index 7e04443..51c9c5d 100644 --- a/lib/jido/sensor/runtime.ex +++ b/lib/jido/sensor/runtime.ex @@ -48,6 +48,8 @@ defmodule Jido.Sensor.Runtime do require Logger + alias Jido.Signal.Dispatch + @type server :: pid() | atom() | {:via, module(), term()} @doc """ @@ -293,8 +295,8 @@ defmodule Jido.Sensor.Runtime do send(agent_ref, {:signal, signal}) agent_ref != nil -> - if Code.ensure_loaded?(Jido.Signal.Dispatch) do - Jido.Signal.Dispatch.dispatch(signal, agent_ref) + if Code.ensure_loaded?(Dispatch) do + Dispatch.dispatch(signal, agent_ref) else Logger.warning("Jido.Signal.Dispatch not available, cannot deliver signal") end diff --git a/lib/jido/skill.ex b/lib/jido/skill.ex index 9c72578..a36c69d 100644 --- a/lib/jido/skill.ex +++ b/lib/jido/skill.ex @@ -316,7 +316,10 @@ defmodule Jido.Skill do subscriptions: 2 ] - defmacro __using__(opts) do + # Helper functions that return quoted expressions to reduce the size of __using__/1 + + @doc false + defp generate_behaviour_and_validation(opts) do quote location: :keep do @behaviour Jido.Skill @@ -339,27 +342,49 @@ defmodule Jido.Skill do end) # Validate actions exist at compile time - @validated_opts.actions - |> Enum.each(fn action_module -> - case Code.ensure_compiled(action_module) do - {:module, _} -> - unless function_exported?(action_module, :__action_metadata__, 0) do - raise CompileError, - description: - "Action #{inspect(action_module)} does not implement Jido.Action behavior", - file: __ENV__.file, - line: __ENV__.line - end - - {:error, reason} -> - raise CompileError, - description: - "Action #{inspect(action_module)} could not be compiled: #{inspect(reason)}", - file: __ENV__.file, - line: __ENV__.line - end - end) + Skill.__validate_actions__(@validated_opts.actions, __ENV__) + end + end + + @doc false + def __validate_actions__(actions, env) do + Enum.each(actions, &validate_single_action(&1, env)) + end + + defp validate_single_action(action_module, env) do + case Code.ensure_compiled(action_module) do + {:module, _} -> + validate_action_behaviour(action_module, env) + + {:error, reason} -> + raise CompileError, + description: + "Action #{inspect(action_module)} could not be compiled: #{inspect(reason)}", + file: env.file, + line: env.line + end + end + defp validate_action_behaviour(action_module, env) do + unless function_exported?(action_module, :__action_metadata__, 0) do + raise CompileError, + description: "Action #{inspect(action_module)} does not implement Jido.Action behavior", + file: env.file, + line: env.line + end + end + + @doc false + defp generate_accessor_functions do + [ + generate_core_accessors(), + generate_optional_accessors(), + generate_list_accessors() + ] + end + + defp generate_core_accessors do + quote location: :keep do @doc "Returns the skill's name." @spec name() :: String.t() def name, do: @validated_opts.name @@ -371,7 +396,11 @@ defmodule Jido.Skill do @doc "Returns the list of action modules provided by this skill." @spec actions() :: [module()] def actions, do: @validated_opts.actions + end + end + defp generate_optional_accessors do + quote location: :keep do @doc "Returns the skill's description." @spec description() :: String.t() | nil def description, do: @validated_opts[:description] @@ -395,7 +424,18 @@ defmodule Jido.Skill do @doc "Returns the Zoi schema for per-agent configuration." @spec config_schema() :: Zoi.schema() | nil def config_schema, do: @validated_opts[:config_schema] + end + end + + defp generate_list_accessors do + [ + generate_pattern_accessors(), + generate_requirement_accessors() + ] + end + defp generate_pattern_accessors do + quote location: :keep do @doc "Returns the signal patterns this skill handles." @spec signal_patterns() :: [String.t()] def signal_patterns, do: @validated_opts[:signal_patterns] || [] @@ -407,7 +447,11 @@ defmodule Jido.Skill do @doc "Returns the capabilities provided by this skill." @spec capabilities() :: [atom()] def capabilities, do: @validated_opts[:capabilities] || [] + end + end + defp generate_requirement_accessors do + quote location: :keep do @doc "Returns the requirements for this skill." @spec requires() :: [tuple()] def requires, do: @validated_opts[:requires] || [] @@ -419,14 +463,19 @@ defmodule Jido.Skill do @doc "Returns the schedules for this skill." @spec schedules() :: [tuple()] def schedules, do: @validated_opts[:schedules] || [] + end + end + @doc false + defp generate_spec_and_manifest_functions do + quote location: :keep do @doc """ Returns the skill specification with optional per-agent configuration. ## Examples - spec = #{inspect(__MODULE__)}.skill_spec(%{}) - spec = #{inspect(__MODULE__)}.skill_spec(%{custom_option: true}) + spec = MyModule.skill_spec(%{}) + spec = MyModule.skill_spec(%{custom_option: true}) """ @spec skill_spec(map()) :: Spec.t() @impl Jido.Skill @@ -491,9 +540,12 @@ defmodule Jido.Skill do tags: tags() } end + end + end - # Default implementations for optional callbacks - + @doc false + defp generate_default_callbacks do + quote location: :keep do @doc false @spec mount(term(), map()) :: {:ok, map() | nil} | {:error, term()} @impl Jido.Skill @@ -524,7 +576,12 @@ defmodule Jido.Skill do @spec subscriptions(map(), map()) :: [{module(), keyword() | map()}] @impl Jido.Skill def subscriptions(_config, _context), do: [] + end + end + @doc false + defp generate_defoverridable do + quote location: :keep do defoverridable mount: 2, router: 1, handle_signal: 2, @@ -548,4 +605,20 @@ defmodule Jido.Skill do schedules: 0 end end + + defmacro __using__(opts) do + behaviour_and_validation = generate_behaviour_and_validation(opts) + accessor_functions = generate_accessor_functions() + spec_and_manifest = generate_spec_and_manifest_functions() + default_callbacks = generate_default_callbacks() + defoverridable_block = generate_defoverridable() + + [ + behaviour_and_validation, + accessor_functions, + spec_and_manifest, + default_callbacks, + defoverridable_block + ] + end end diff --git a/lib/jido/skill/instance.ex b/lib/jido/skill/instance.ex index ee6f535..7e86f1b 100644 --- a/lib/jido/skill/instance.ex +++ b/lib/jido/skill/instance.ex @@ -26,6 +26,8 @@ defmodule Jido.Skill.Instance do Instance.new({MySkill, as: :sales, token: "sales-token"}) """ + alias Jido.Skill.Config + @schema Zoi.struct( __MODULE__, %{ @@ -83,7 +85,7 @@ defmodule Jido.Skill.Instance do base_state_key = manifest.state_key base_name = manifest.name - resolved_config = Jido.Skill.Config.resolve_config!(module, overrides) + resolved_config = Config.resolve_config!(module, overrides) state_key = derive_state_key(base_state_key, as_opt) route_prefix = derive_route_prefix(base_name, as_opt) diff --git a/lib/jido/skill/requirements.ex b/lib/jido/skill/requirements.ex index f04e1bd..eee874e 100644 --- a/lib/jido/skill/requirements.ex +++ b/lib/jido/skill/requirements.ex @@ -136,12 +136,10 @@ defmodule Jido.Skill.Requirements do @spec format_error(%{String.t() => [requirement()]}) :: String.t() def format_error(missing_by_skill) do parts = - missing_by_skill - |> Enum.map(fn {skill_name, requirements} -> - reqs_str = requirements |> Enum.map(&inspect/1) |> Enum.join(", ") + Enum.map_join(missing_by_skill, "; ", fn {skill_name, requirements} -> + reqs_str = Enum.map_join(requirements, ", ", &inspect/1) "#{skill_name} requires #{reqs_str}" end) - |> Enum.join("; ") "Missing requirements for skills: #{parts}" end diff --git a/lib/jido/skill/routes.ex b/lib/jido/skill/routes.ex index 98737a3..e25387e 100644 --- a/lib/jido/skill/routes.ex +++ b/lib/jido/skill/routes.ex @@ -203,23 +203,31 @@ defmodule Jido.Skill.Routes do winner = Enum.max_by(replace_routes, fn {_, _, priority, _} -> priority end) {:ok, winner} else - sorted = Enum.sort_by(routes, fn {_, _, priority, _} -> priority end, :desc) - [highest | rest] = sorted - - {_, _, highest_priority, _} = highest - same_priority = Enum.filter(rest, fn {_, _, p, _} -> p == highest_priority end) - - if same_priority == [] do - {:ok, highest} - else - targets = - [highest | same_priority] - |> Enum.map(fn {_, target, _, _} -> inspect(target) end) - |> Enum.join(", ") - - {:error, - "Route conflict: '#{path}' defined multiple times with same priority #{highest_priority} (targets: #{targets})"} - end + resolve_by_priority(path, routes) end end + + defp resolve_by_priority(path, routes) do + sorted = Enum.sort_by(routes, fn {_, _, priority, _} -> priority end, :desc) + [highest | rest] = sorted + + {_, _, highest_priority, _} = highest + same_priority = Enum.filter(rest, fn {_, _, p, _} -> p == highest_priority end) + + if same_priority == [] do + {:ok, highest} + else + build_conflict_error(path, highest, same_priority, highest_priority) + end + end + + defp build_conflict_error(path, highest, same_priority, priority) do + targets = + Enum.map_join([highest | same_priority], ", ", fn {_, target, _, _} -> + inspect(target) + end) + + {:error, + "Route conflict: '#{path}' defined multiple times with same priority #{priority} (targets: #{targets})"} + end end diff --git a/lib/jido/util.ex b/lib/jido/util.ex index 574850c..6e60232 100644 --- a/lib/jido/util.ex +++ b/lib/jido/util.ex @@ -18,6 +18,8 @@ defmodule Jido.Util do but they can also be useful for developers building applications with Jido. """ + alias Jido.Signal.ID, as: SignalID + require OK require Logger @@ -27,7 +29,7 @@ defmodule Jido.Util do Generates a unique ID. """ @spec generate_id() :: String.t() - def generate_id, do: Jido.Signal.ID.generate!() + def generate_id, do: SignalID.generate!() @doc """ Converts a string to a binary. diff --git a/lib/mix/tasks/jido.gen.agent.ex b/lib/mix/tasks/jido.gen.agent.ex index 4009207..df1b9bc 100644 --- a/lib/mix/tasks/jido.gen.agent.ex +++ b/lib/mix/tasks/jido.gen.agent.ex @@ -19,6 +19,9 @@ if Code.ensure_loaded?(Igniter) do use Igniter.Mix.Task + alias Igniter.Project.Module, as: IgniterModule + alias Jido.Igniter.Helpers + @impl Igniter.Mix.Task def info(_argv, _composing_task) do %Igniter.Mix.Task.Info{ @@ -40,12 +43,12 @@ if Code.ensure_loaded?(Igniter) do positional = igniter.args.positional module_name = positional[:module] - module = Igniter.Project.Module.parse(module_name) - name = Jido.Igniter.Helpers.module_to_name(module_name) + module = IgniterModule.parse(module_name) + name = Helpers.module_to_name(module_name) skills = options[:skills] - |> Jido.Igniter.Helpers.parse_list() + |> Helpers.parse_list() |> Enum.map(&String.to_atom/1) skills_opt = @@ -66,7 +69,7 @@ if Code.ensure_loaded?(Igniter) do """ test_module_name = "JidoTest.#{module_name |> String.replace(~r/^.*?\./, "")}" - test_module = Igniter.Project.Module.parse(test_module_name) + test_module = IgniterModule.parse(test_module_name) agent_alias = module |> Module.split() |> List.last() @@ -91,8 +94,8 @@ if Code.ensure_loaded?(Igniter) do """ igniter - |> Igniter.Project.Module.create_module(module, contents) - |> Igniter.Project.Module.create_module(test_module, test_contents, location: :test) + |> IgniterModule.create_module(module, contents) + |> IgniterModule.create_module(test_module, test_contents, location: :test) end end end diff --git a/lib/mix/tasks/jido.gen.sensor.ex b/lib/mix/tasks/jido.gen.sensor.ex index 2895289..4d39a03 100644 --- a/lib/mix/tasks/jido.gen.sensor.ex +++ b/lib/mix/tasks/jido.gen.sensor.ex @@ -19,6 +19,9 @@ if Code.ensure_loaded?(Igniter) do use Igniter.Mix.Task + alias Igniter.Project.Module, as: IgniterModule + alias Jido.Igniter.Helpers + @impl Igniter.Mix.Task def info(_argv, _composing_task) do %Igniter.Mix.Task.Info{ @@ -40,8 +43,8 @@ if Code.ensure_loaded?(Igniter) do positional = igniter.args.positional module_name = positional[:module] - module = Igniter.Project.Module.parse(module_name) - name = Jido.Igniter.Helpers.module_to_name(module_name) + module = IgniterModule.parse(module_name) + name = Helpers.module_to_name(module_name) interval = options[:interval] contents = """ @@ -68,7 +71,7 @@ if Code.ensure_loaded?(Igniter) do """ test_module_name = "JidoTest.#{module_name |> String.replace(~r/^.*?\./, "")}" - test_module = Igniter.Project.Module.parse(test_module_name) + test_module = IgniterModule.parse(test_module_name) sensor_alias = module |> Module.split() |> List.last() @@ -97,8 +100,8 @@ if Code.ensure_loaded?(Igniter) do """ igniter - |> Igniter.Project.Module.create_module(module, contents) - |> Igniter.Project.Module.create_module(test_module, test_contents, location: :test) + |> IgniterModule.create_module(module, contents) + |> IgniterModule.create_module(test_module, test_contents, location: :test) end end end diff --git a/lib/mix/tasks/jido.gen.skill.ex b/lib/mix/tasks/jido.gen.skill.ex index 35cf222..52a942e 100644 --- a/lib/mix/tasks/jido.gen.skill.ex +++ b/lib/mix/tasks/jido.gen.skill.ex @@ -19,6 +19,9 @@ if Code.ensure_loaded?(Igniter) do use Igniter.Mix.Task + alias Igniter.Project.Module, as: IgniterModule + alias Jido.Igniter.Helpers + @impl Igniter.Mix.Task def info(_argv, _composing_task) do %Igniter.Mix.Task.Info{ @@ -40,16 +43,13 @@ if Code.ensure_loaded?(Igniter) do positional = igniter.args.positional module_name = positional[:module] - module = Igniter.Project.Module.parse(module_name) - name = Jido.Igniter.Helpers.module_to_name(module_name) + module = IgniterModule.parse(module_name) + name = Helpers.module_to_name(module_name) state_key = name - signal_patterns = Jido.Igniter.Helpers.parse_list(options[:signals]) + signal_patterns = Helpers.parse_list(options[:signals]) - patterns_str = - signal_patterns - |> Enum.map(&~s("#{&1}")) - |> Enum.join(", ") + patterns_str = Enum.map_join(signal_patterns, ", ", &~s("#{&1}")) contents = """ defmodule #{inspect(module)} do @@ -68,7 +68,7 @@ if Code.ensure_loaded?(Igniter) do """ test_module_name = "JidoTest.#{module_name |> String.replace(~r/^.*?\./, "")}" - test_module = Igniter.Project.Module.parse(test_module_name) + test_module = IgniterModule.parse(test_module_name) skill_alias = module |> Module.split() |> List.last() @@ -95,8 +95,8 @@ if Code.ensure_loaded?(Igniter) do """ igniter - |> Igniter.Project.Module.create_module(module, contents) - |> Igniter.Project.Module.create_module(test_module, test_contents, location: :test) + |> IgniterModule.create_module(module, contents) + |> IgniterModule.create_module(test_module, test_contents, location: :test) end end end diff --git a/lib/mix/tasks/jido.install.ex b/lib/mix/tasks/jido.install.ex index 3c28874..572265e 100644 --- a/lib/mix/tasks/jido.install.ex +++ b/lib/mix/tasks/jido.install.ex @@ -27,6 +27,9 @@ if Code.ensure_loaded?(Igniter) do use Igniter.Mix.Task + alias Igniter.Project.Application + alias Igniter.Project.Config + @impl Igniter.Mix.Task def info(_argv, _composing_task) do %Igniter.Mix.Task.Info{ @@ -48,11 +51,11 @@ if Code.ensure_loaded?(Igniter) do @impl Igniter.Mix.Task def igniter(igniter) do options = igniter.args.options - app_name = Igniter.Project.Application.app_name(igniter) + app_name = Application.app_name(igniter) igniter = igniter - |> Igniter.Project.Config.configure_new( + |> Config.configure_new( "config.exs", :jido, [:default_bus], @@ -63,7 +66,7 @@ if Code.ensure_loaded?(Igniter) do if options[:no_supervisor] do igniter else - Igniter.Project.Application.add_new_child( + Application.add_new_child( igniter, {Jido.Bus.InMemory, name: :jido_bus}, after: [Ecto.Repo, Phoenix.PubSub] diff --git a/test/examples/emit_directive_test.exs b/test/examples/emit_directive_test.exs index 6dd16e5..8fe54d4 100644 --- a/test/examples/emit_directive_test.exs +++ b/test/examples/emit_directive_test.exs @@ -15,9 +15,9 @@ defmodule JidoExampleTest.EmitDirectiveTest do @moduletag :example @moduletag timeout: 15_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Emit domain events @@ -196,7 +196,7 @@ defmodule JidoExampleTest.EmitDirectiveTest do eventually( fn -> signals = SignalCollector.get_signals(collector) - length(signals) >= 1 + signals != [] end, timeout: 2_000 ) diff --git a/test/examples/error_handling_test.exs b/test/examples/error_handling_test.exs index 3896281..8eece9f 100644 --- a/test/examples/error_handling_test.exs +++ b/test/examples/error_handling_test.exs @@ -28,9 +28,9 @@ defmodule JidoExampleTest.ErrorHandlingTest do @moduletag :example @moduletag timeout: 15_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Error handling patterns diff --git a/test/examples/hierarchical_agents_test.exs b/test/examples/hierarchical_agents_test.exs index 9acb092..95674ea 100644 --- a/test/examples/hierarchical_agents_test.exs +++ b/test/examples/hierarchical_agents_test.exs @@ -54,10 +54,10 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do @moduletag :example @moduletag timeout: 30_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.Agent.StateOp alias Jido.AgentServer + alias Jido.Signal alias Jido.Tracing.Trace # =========================================================================== @@ -510,7 +510,7 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do fn -> case AgentServer.state(orchestrator_pid) do {:ok, %{agent: %{state: %{completed_jobs: jobs}}}} -> - length(jobs) >= 1 + jobs != [] _ -> false @@ -560,8 +560,8 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do eventually( fn -> case AgentServer.state(orchestrator_pid) do - {:ok, %{agent: %{state: %{completed_jobs: jobs}}}} -> - length(jobs) >= 2 + {:ok, %{agent: %{state: %{completed_jobs: [_, _ | _]}}}} -> + true _ -> false @@ -609,7 +609,7 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do fn -> case AgentServer.state(orchestrator_pid) do {:ok, %{agent: %{state: %{completed_jobs: jobs}}}} -> - length(jobs) >= 1 + jobs != [] _ -> false @@ -619,7 +619,7 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do ) {:ok, final_state} = AgentServer.state(orchestrator_pid) - assert length(final_state.agent.state.completed_jobs) >= 1 + assert final_state.agent.state.completed_jobs != [] end end @@ -648,7 +648,7 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do fn -> case AgentServer.state(orchestrator_pid) do {:ok, %{agent: %{state: %{completed_jobs: jobs}}}} -> - length(jobs) >= 1 + jobs != [] _ -> false @@ -695,7 +695,7 @@ defmodule JidoExampleTest.HierarchicalAgentsTest do fn -> case AgentServer.state(orchestrator_pid) do {:ok, %{agent: %{state: %{completed_jobs: jobs}}}} -> - length(jobs) >= 1 + jobs != [] _ -> false diff --git a/test/examples/observability_test.exs b/test/examples/observability_test.exs index 49139ab..7546cce 100644 --- a/test/examples/observability_test.exs +++ b/test/examples/observability_test.exs @@ -16,9 +16,9 @@ defmodule JidoExampleTest.ObservabilityTest do @moduletag :example @moduletag timeout: 15_000 + alias Jido.AgentServer alias Jido.Observe alias Jido.Signal - alias Jido.AgentServer alias Jido.Tracing.Context, as: TraceContext # =========================================================================== @@ -317,7 +317,7 @@ defmodule JidoExampleTest.ObservabilityTest do eventually(fn -> events = TelemetryCollector.get_events(collector) - length(events) >= 1 + events != [] end) [{event, measurements, metadata}] = TelemetryCollector.get_events(collector) diff --git a/test/examples/parent_child_test.exs b/test/examples/parent_child_test.exs index 60bcb64..95e9861 100644 --- a/test/examples/parent_child_test.exs +++ b/test/examples/parent_child_test.exs @@ -42,10 +42,10 @@ defmodule JidoExampleTest.ParentChildTest do @moduletag :example @moduletag timeout: 25_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.Agent.StateOp alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Worker operations @@ -264,7 +264,7 @@ defmodule JidoExampleTest.ParentChildTest do fn -> case AgentServer.state(coordinator_pid) do {:ok, %{agent: %{state: %{completed_responses: responses}}}} -> - length(responses) >= 1 + responses != [] _ -> false @@ -299,7 +299,7 @@ defmodule JidoExampleTest.ParentChildTest do fn -> case AgentServer.state(coordinator_pid) do {:ok, %{agent: %{state: %{completed_responses: responses}}}} -> - length(responses) >= 1 + responses != [] _ -> false @@ -345,7 +345,7 @@ defmodule JidoExampleTest.ParentChildTest do fn -> case AgentServer.state(coordinator_pid) do {:ok, %{agent: %{state: %{completed_responses: responses}}}} -> - length(responses) >= 3 + match?([_, _, _ | _], responses) _ -> false @@ -392,7 +392,7 @@ defmodule JidoExampleTest.ParentChildTest do fn -> case AgentServer.state(coordinator_pid) do {:ok, %{agent: %{state: %{completed_responses: responses}}}} -> - length(responses) >= 3 + match?([_, _, _ | _], responses) _ -> false diff --git a/test/examples/schedule_directive_test.exs b/test/examples/schedule_directive_test.exs index ddd9648..0c19105 100644 --- a/test/examples/schedule_directive_test.exs +++ b/test/examples/schedule_directive_test.exs @@ -15,9 +15,9 @@ defmodule JidoExampleTest.ScheduleDirectiveTest do @moduletag :example @moduletag timeout: 20_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Schedule-based patterns diff --git a/test/examples/sensor_demo_test.exs b/test/examples/sensor_demo_test.exs index 043be91..491f338 100644 --- a/test/examples/sensor_demo_test.exs +++ b/test/examples/sensor_demo_test.exs @@ -14,8 +14,8 @@ defmodule JidoExampleTest.SensorDemoTest do @moduletag :example @moduletag timeout: 30_000 - alias Jido.Signal alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Handle sensor and webhook signals @@ -139,8 +139,8 @@ defmodule JidoExampleTest.SensorDemoTest do @moduledoc false use GenServer - alias Jido.Signal alias Jido.AgentServer + alias Jido.Signal @quotes [ "The best way to predict the future is to create it.", @@ -199,8 +199,8 @@ defmodule JidoExampleTest.SensorDemoTest do defmodule WebhookHelper do @moduledoc false - alias Jido.Signal alias Jido.AgentServer + alias Jido.Signal def emit_github_event(agent_target, event_type, payload) do signal = @@ -257,7 +257,7 @@ defmodule JidoExampleTest.SensorDemoTest do agent_pid, fn state -> quotes = state.agent.state.quotes - length(quotes) >= 2 + match?([_, _ | _], quotes) end, timeout: 5_000, interval: 50 @@ -298,7 +298,7 @@ defmodule JidoExampleTest.SensorDemoTest do agent_pid, fn state -> events = state.agent.state.events - length(events) >= 2 + match?([_, _ | _], events) end, timeout: 5_000, interval: 50 @@ -338,7 +338,7 @@ defmodule JidoExampleTest.SensorDemoTest do # Wait for some quotes, then inject webhooks eventually_state( agent_pid, - fn state -> length(state.agent.state.quotes) >= 1 end, + fn state -> state.agent.state.quotes != [] end, timeout: 3_000, interval: 50 ) diff --git a/test/examples/signal_routing_test.exs b/test/examples/signal_routing_test.exs index dc75cef..31f4233 100644 --- a/test/examples/signal_routing_test.exs +++ b/test/examples/signal_routing_test.exs @@ -15,8 +15,8 @@ defmodule JidoExampleTest.SignalRoutingTest do @moduletag :example @moduletag timeout: 15_000 - alias Jido.Signal alias Jido.AgentServer + alias Jido.Signal # =========================================================================== # ACTIONS: Handle different signal types diff --git a/test/examples/spawn_agent_test.exs b/test/examples/spawn_agent_test.exs index 18b6871..f709c7d 100644 --- a/test/examples/spawn_agent_test.exs +++ b/test/examples/spawn_agent_test.exs @@ -25,10 +25,10 @@ defmodule JidoExampleTest.SpawnAgentTest do @moduletag :example @moduletag timeout: 20_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.AgentServer alias Jido.AgentServer.ParentRef + alias Jido.Signal # =========================================================================== # ACTIONS: Parent actions for spawning and managing children @@ -241,7 +241,7 @@ defmodule JidoExampleTest.SpawnAgentTest do child_info = await_child(parent_pid, :notified_worker) eventually_state(parent_pid, fn state -> - length(state.agent.state.child_started_events) > 0 + state.agent.state.child_started_events != [] end) {:ok, state} = AgentServer.state(parent_pid) @@ -321,7 +321,7 @@ defmodule JidoExampleTest.SpawnAgentTest do assert child_agent.state.last_task == "process data" eventually_state(parent_pid, fn state -> - length(state.agent.state.worker_results) > 0 + state.agent.state.worker_results != [] end) {:ok, parent_state} = AgentServer.state(parent_pid) diff --git a/test/examples/tracing_test.exs b/test/examples/tracing_test.exs index 3a26f5c..cd964ba 100644 --- a/test/examples/tracing_test.exs +++ b/test/examples/tracing_test.exs @@ -22,9 +22,9 @@ defmodule JidoExampleTest.TracingTest do @moduletag :example @moduletag timeout: 15_000 - alias Jido.Signal alias Jido.Agent.Directive alias Jido.AgentServer + alias Jido.Signal alias Jido.Tracing.Trace # =========================================================================== @@ -180,7 +180,7 @@ defmodule JidoExampleTest.TracingTest do eventually(fn -> signals = TracedSignalCollector.get_signals(collector) - length(signals) >= 1 + signals != [] end) [emitted] = TracedSignalCollector.get_signals(collector) @@ -214,7 +214,7 @@ defmodule JidoExampleTest.TracingTest do eventually(fn -> signals = TracedSignalCollector.get_signals(collector) - length(signals) >= 3 + match?([_, _, _ | _], signals) end) signals = TracedSignalCollector.get_signals(collector) @@ -251,7 +251,7 @@ defmodule JidoExampleTest.TracingTest do eventually(fn -> signals = TracedSignalCollector.get_signals(collector) - length(signals) >= 1 + signals != [] end) [emitted] = TracedSignalCollector.get_signals(collector) @@ -281,7 +281,7 @@ defmodule JidoExampleTest.TracingTest do eventually(fn -> signals = TracedSignalCollector.get_signals(collector) - length(signals) >= 1 + signals != [] end) [emitted] = TracedSignalCollector.get_signals(collector) diff --git a/test/jido/agent/agent_test.exs b/test/jido/agent/agent_test.exs index b2cb3ae..38445b9 100644 --- a/test/jido/agent/agent_test.exs +++ b/test/jido/agent/agent_test.exs @@ -2,8 +2,8 @@ defmodule JidoTest.AgentTest do use ExUnit.Case, async: true alias Jido.Agent - alias JidoTest.TestAgents alias JidoTest.TestActions + alias JidoTest.TestAgents describe "module definition" do test "defines metadata accessors" do @@ -253,7 +253,7 @@ defmodule JidoTest.AgentTest do end test "Agent.new/1 returns error for invalid id type" do - {:error, error} = Agent.new(%{id: 12345}) + {:error, error} = Agent.new(%{id: 12_345}) assert error.message == "Agent validation failed" end diff --git a/test/jido/agent/directive_test.exs b/test/jido/agent/directive_test.exs index d5502be..e877723 100644 --- a/test/jido/agent/directive_test.exs +++ b/test/jido/agent/directive_test.exs @@ -2,6 +2,7 @@ defmodule JidoTest.Agent.DirectiveTest do use ExUnit.Case, async: true alias Jido.Agent.Directive + alias Jido.AgentServer.ParentRef describe "emit/2" do test "creates Emit directive without dispatch" do @@ -157,7 +158,7 @@ defmodule JidoTest.Agent.DirectiveTest do parent_pid = self() parent_ref = - Jido.AgentServer.ParentRef.new!(%{pid: parent_pid, id: "parent-123", tag: :child}) + ParentRef.new!(%{pid: parent_pid, id: "parent-123", tag: :child}) agent = %{state: %{__parent__: parent_ref}} signal = %{type: "child.result"} @@ -172,7 +173,7 @@ defmodule JidoTest.Agent.DirectiveTest do parent_pid = self() parent_ref = - Jido.AgentServer.ParentRef.new!(%{pid: parent_pid, id: "parent-123", tag: :child}) + ParentRef.new!(%{pid: parent_pid, id: "parent-123", tag: :child}) agent = %{state: %{__parent__: parent_ref}} diff --git a/test/jido/agent/instance_manager_test.exs b/test/jido/agent/instance_manager_test.exs index 4b5672b..f8260c3 100644 --- a/test/jido/agent/instance_manager_test.exs +++ b/test/jido/agent/instance_manager_test.exs @@ -7,6 +7,7 @@ defmodule JidoTest.Agent.InstanceManagerTest do @moduletag :integration alias Jido.Agent.InstanceManager + alias Jido.Agent.Store.ETS alias Jido.AgentServer # Use module attribute for manager naming to avoid atom leaks @@ -344,7 +345,7 @@ defmodule JidoTest.Agent.InstanceManagerTest do # Verify state was persisted to ETS store_key = {TestAgent, "stop-persist-key"} - case Jido.Agent.Store.ETS.get(store_key, table: table) do + case ETS.get(store_key, table: table) do {:ok, persisted} -> # Persisted data should contain the counter assert persisted.state.counter == 42 diff --git a/test/jido/agent/state_ops_test.exs b/test/jido/agent/state_ops_test.exs index f40d395..7efb445 100644 --- a/test/jido/agent/state_ops_test.exs +++ b/test/jido/agent/state_ops_test.exs @@ -2,9 +2,9 @@ defmodule JidoTest.Agent.StateOpsTest do use ExUnit.Case, async: true alias Jido.Agent - alias Jido.Agent.StateOps - alias Jido.Agent.StateOp alias Jido.Agent.Directive + alias Jido.Agent.StateOp + alias Jido.Agent.StateOps describe "apply_result/2" do test "merges result into agent state" do diff --git a/test/jido/agent_server/agent_server_coverage_test.exs b/test/jido/agent_server/agent_server_coverage_test.exs index 8c1b46b..63943e4 100644 --- a/test/jido/agent_server/agent_server_coverage_test.exs +++ b/test/jido/agent_server/agent_server_coverage_test.exs @@ -18,6 +18,7 @@ defmodule JidoTest.AgentServerCoverageTest do alias Jido.AgentServer alias Jido.Signal + alias JidoTest.TestAgents.Counter # Simple test agent with defaults defmodule SimpleTestAgent do @@ -201,7 +202,7 @@ defmodule JidoTest.AgentServerCoverageTest do test "via tuple that exists works", %{jido: jido} do {:ok, _pid} = AgentServer.start_link( - agent: JidoTest.TestAgents.Counter, + agent: Counter, id: "via-test-exists", jido: jido ) @@ -269,20 +270,20 @@ defmodule JidoTest.AgentServerCoverageTest do describe "pre-built struct with agent_module option" do test "uses explicit agent_module for cmd routing", %{jido: jido} do - agent = JidoTest.TestAgents.Counter.new(id: "prebuilt-struct-test") + agent = Counter.new(id: "prebuilt-struct-test") agent = %{agent | state: Map.put(agent.state, :counter, 50)} {:ok, pid} = AgentServer.start_link( agent: agent, - agent_module: JidoTest.TestAgents.Counter, + agent_module: Counter, jido: jido ) {:ok, state} = AgentServer.state(pid) assert state.id == "prebuilt-struct-test" assert state.agent.state.counter == 50 - assert state.agent_module == JidoTest.TestAgents.Counter + assert state.agent_module == Counter signal = Signal.new!("increment", %{}, source: "/test") {:ok, updated_agent} = AgentServer.call(pid, signal) @@ -400,7 +401,7 @@ defmodule JidoTest.AgentServerCoverageTest do test "alive? returns true for existing via tuple", %{jido: jido} do {:ok, _pid} = AgentServer.start_link( - agent: JidoTest.TestAgents.Counter, + agent: Counter, id: "alive-via-test", jido: jido ) diff --git a/test/jido/agent_server/agent_server_stop_log_test.exs b/test/jido/agent_server/agent_server_stop_log_test.exs index 6784548..86bdce6 100644 --- a/test/jido/agent_server/agent_server_stop_log_test.exs +++ b/test/jido/agent_server/agent_server_stop_log_test.exs @@ -3,8 +3,8 @@ defmodule JidoTest.AgentServerStopLogTest do import ExUnit.CaptureLog - alias Jido.AgentServer alias Jido.Agent.Directive + alias Jido.AgentServer alias Jido.Signal defmodule StopTestAction do diff --git a/test/jido/agent_server/agent_server_test.exs b/test/jido/agent_server/agent_server_test.exs index d9115da..273423a 100644 --- a/test/jido/agent_server/agent_server_test.exs +++ b/test/jido/agent_server/agent_server_test.exs @@ -3,9 +3,9 @@ defmodule JidoTest.AgentServerTest do @moduletag :capture_log + alias Jido.Agent.Directive alias Jido.AgentServer alias Jido.AgentServer.State - alias Jido.Agent.Directive alias Jido.Signal alias JidoTest.TestActions diff --git a/test/jido/agent_server/cron_integration_test.exs b/test/jido/agent_server/cron_integration_test.exs index 12db7a5..b092f8a 100644 --- a/test/jido/agent_server/cron_integration_test.exs +++ b/test/jido/agent_server/cron_integration_test.exs @@ -4,8 +4,8 @@ defmodule JidoTest.AgentServer.CronIntegrationTest do @moduletag :integration @moduletag capture_log: true - alias Jido.AgentServer alias Jido.Agent.Directive + alias Jido.AgentServer alias Jido.Signal defmodule CronCountAction do diff --git a/test/jido/agent_server/directive_exec_test.exs b/test/jido/agent_server/directive_exec_test.exs index f1ba4ed..3ae9c9e 100644 --- a/test/jido/agent_server/directive_exec_test.exs +++ b/test/jido/agent_server/directive_exec_test.exs @@ -2,7 +2,7 @@ defmodule JidoTest.AgentServer.DirectiveExecTest do use JidoTest.Case, async: true alias Jido.Agent.Directive - alias Jido.AgentServer.{DirectiveExec, State, Options} + alias Jido.AgentServer.{DirectiveExec, Options, State} alias Jido.Signal defmodule TestAgent do diff --git a/test/jido/agent_server/error_policy_test.exs b/test/jido/agent_server/error_policy_test.exs index 5fd0c36..534184f 100644 --- a/test/jido/agent_server/error_policy_test.exs +++ b/test/jido/agent_server/error_policy_test.exs @@ -4,7 +4,7 @@ defmodule JidoTest.AgentServer.ErrorPolicyTest do @moduletag :capture_log alias Jido.Agent.Directive - alias Jido.AgentServer.{ErrorPolicy, State, Options} + alias Jido.AgentServer.{ErrorPolicy, Options, State} defmodule TestAgent do @moduledoc false diff --git a/test/jido/agent_server/hierarchy_test.exs b/test/jido/agent_server/hierarchy_test.exs index 2e7626f..029351e 100644 --- a/test/jido/agent_server/hierarchy_test.exs +++ b/test/jido/agent_server/hierarchy_test.exs @@ -3,9 +3,10 @@ defmodule JidoTest.AgentServer.HierarchyTest do @moduletag capture_log: true + alias Jido.Agent.Directive alias Jido.AgentServer + alias Jido.AgentServer.ChildInfo alias Jido.AgentServer.{ParentRef, State} - alias Jido.Agent.Directive alias Jido.Signal # Actions for ParentAgent @@ -278,7 +279,7 @@ defmodule JidoTest.AgentServer.HierarchyTest do ref = Process.monitor(child_pid) child_info = - Jido.AgentServer.ChildInfo.new!(%{ + ChildInfo.new!(%{ pid: child_pid, ref: ref, module: ChildAgent, @@ -320,7 +321,7 @@ defmodule JidoTest.AgentServer.HierarchyTest do ref = Process.monitor(child_pid) child_info = - Jido.AgentServer.ChildInfo.new!(%{ + ChildInfo.new!(%{ pid: child_pid, ref: ref, module: ChildAgent, diff --git a/test/jido/agent_server/signal_router_test.exs b/test/jido/agent_server/signal_router_test.exs index ada9cdf..a8db25b 100644 --- a/test/jido/agent_server/signal_router_test.exs +++ b/test/jido/agent_server/signal_router_test.exs @@ -3,6 +3,7 @@ defmodule JidoTest.AgentServer.SignalRouterTest do alias Jido.AgentServer.SignalRouter alias Jido.AgentServer.State + alias Jido.AgentServer.State.Lifecycle alias Jido.Signal.Router, as: JidoRouter # ============================================================================= @@ -226,7 +227,7 @@ defmodule JidoTest.AgentServer.SignalRouterTest do defp build_test_state(agent_module) do agent = agent_module.new(%{id: "test-#{System.unique_integer([:positive])}"}) - {:ok, lifecycle} = Jido.AgentServer.State.Lifecycle.new([]) + {:ok, lifecycle} = Lifecycle.new([]) attrs = %{ id: agent.id, diff --git a/test/jido/agent_server/skill_subscriptions_test.exs b/test/jido/agent_server/skill_subscriptions_test.exs index 855bab6..14f5f63 100644 --- a/test/jido/agent_server/skill_subscriptions_test.exs +++ b/test/jido/agent_server/skill_subscriptions_test.exs @@ -1,6 +1,8 @@ defmodule JidoTest.AgentServer.SkillSubscriptionsTest do use JidoTest.Case, async: false + alias Jido.Sensor.Runtime + @moduletag :capture_log # --------------------------------------------------------------------------- @@ -297,7 +299,7 @@ defmodule JidoTest.AgentServer.SkillSubscriptionsTest do [{_tag, child_info}] = sensor_children - Jido.Sensor.Runtime.event(child_info.pid, {:trigger, :test_value}) + Runtime.event(child_info.pid, {:trigger, :test_value}) Process.sleep(50) diff --git a/test/jido/agent_server/strategy_init_test.exs b/test/jido/agent_server/strategy_init_test.exs index a38f65e..dd3a197 100644 --- a/test/jido/agent_server/strategy_init_test.exs +++ b/test/jido/agent_server/strategy_init_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.AgentServer.StrategyInitTest do use JidoTest.Case, async: true - alias Jido.AgentServer alias Jido.Agent.Directive + alias Jido.AgentServer alias Jido.Signal defmodule TrackingStrategy do diff --git a/test/jido/agent_server/telemetry_test.exs b/test/jido/agent_server/telemetry_test.exs index 47a59f9..bb075e7 100644 --- a/test/jido/agent_server/telemetry_test.exs +++ b/test/jido/agent_server/telemetry_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.AgentServer.TelemetryTest do use JidoTest.Case, async: false - alias Jido.AgentServer alias Jido.Agent.Directive + alias Jido.AgentServer alias Jido.Signal alias JidoTest.TestActions diff --git a/test/jido/agent_server/trace_propagation_test.exs b/test/jido/agent_server/trace_propagation_test.exs index bf50858..0dd2aff 100644 --- a/test/jido/agent_server/trace_propagation_test.exs +++ b/test/jido/agent_server/trace_propagation_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.AgentServer.TracePropagationTest do use JidoTest.Case, async: false - alias Jido.AgentServer alias Jido.Agent.Directive + alias Jido.AgentServer alias Jido.Signal alias Jido.Tracing.Trace alias JidoTest.TestActions diff --git a/test/jido/agent_skill_integration_test.exs b/test/jido/agent_skill_integration_test.exs index 9a2cfce..b780877 100644 --- a/test/jido/agent_skill_integration_test.exs +++ b/test/jido/agent_skill_integration_test.exs @@ -1,6 +1,7 @@ defmodule JidoTest.AgentSkillIntegrationTest do use ExUnit.Case, async: true + alias Jido.Agent.Schema alias Jido.Skill.Spec # ============================================================================= @@ -224,7 +225,7 @@ defmodule JidoTest.AgentSkillIntegrationTest do schema = SingleSkillAgent.schema() assert is_struct(schema) - keys = Jido.Agent.Schema.known_keys(schema) + keys = Schema.known_keys(schema) assert :counter_skill in keys end @@ -608,7 +609,7 @@ defmodule JidoTest.AgentSkillIntegrationTest do describe "schema merging" do test "merged schema contains skill state_keys" do schema = MixedSchemaAgent.schema() - keys = Jido.Agent.Schema.known_keys(schema) + keys = Schema.known_keys(schema) assert :counter_skill in keys end diff --git a/test/jido/await_coverage_test.exs b/test/jido/await_coverage_test.exs index 8a816d3..4f00c12 100644 --- a/test/jido/await_coverage_test.exs +++ b/test/jido/await_coverage_test.exs @@ -5,8 +5,8 @@ defmodule JidoTest.AwaitCoverageTest do """ use JidoTest.Case, async: false - alias Jido.Await alias Jido.AgentServer + alias Jido.Await alias Jido.Signal defmodule NeverCompletesAction do diff --git a/test/jido/await_test.exs b/test/jido/await_test.exs index 3feafe4..0e17fbf 100644 --- a/test/jido/await_test.exs +++ b/test/jido/await_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.AwaitTest do use JidoTest.Case, async: false - alias Jido.Await alias Jido.AgentServer + alias Jido.Await alias Jido.Signal defmodule CompletingAction do diff --git a/test/jido/discovery_test.exs b/test/jido/discovery_test.exs index 0d75957..a1c40ed 100644 --- a/test/jido/discovery_test.exs +++ b/test/jido/discovery_test.exs @@ -149,7 +149,7 @@ defmodule JidoTest.DiscoveryTest do test "returns action for valid slug" do actions = Discovery.list_actions(limit: 1) - if length(actions) > 0 do + if actions != [] do [action | _] = actions found = Discovery.get_action_by_slug(action.slug) assert found != nil @@ -172,7 +172,7 @@ defmodule JidoTest.DiscoveryTest do test "returns agent for valid slug" do agents = Discovery.list_agents(limit: 1) - if length(agents) > 0 do + if agents != [] do [agent | _] = agents found = Discovery.get_agent_by_slug(agent.slug) assert found != nil diff --git a/test/jido/error_coverage_test.exs b/test/jido/error_coverage_test.exs index 2329a9c..bfd74ee 100644 --- a/test/jido/error_coverage_test.exs +++ b/test/jido/error_coverage_test.exs @@ -2,6 +2,7 @@ defmodule JidoTest.ErrorCoverageTest do use JidoTest.Case, async: true alias Jido.Error + alias Jido.Error.Internal.UnknownError describe "error constructors with map options" do test "validation_error accepts map opts" do @@ -253,7 +254,7 @@ defmodule JidoTest.ErrorCoverageTest do end test "UnknownError returns :internal" do - error = Error.Internal.UnknownError.exception(message: "Unknown") + error = UnknownError.exception(message: "Unknown") result = Error.to_map(error) assert result.type == :internal end diff --git a/test/jido/error_test.exs b/test/jido/error_test.exs index 625b698..33956d2 100644 --- a/test/jido/error_test.exs +++ b/test/jido/error_test.exs @@ -264,7 +264,7 @@ defmodule JidoTest.ErrorTest do stacktrace = Error.capture_stacktrace() assert is_list(stacktrace) - assert length(stacktrace) > 0 + assert stacktrace != [] end end end diff --git a/test/jido/observe/observe_coverage_test.exs b/test/jido/observe/observe_coverage_test.exs index 42695a3..d5d8e0d 100644 --- a/test/jido/observe/observe_coverage_test.exs +++ b/test/jido/observe/observe_coverage_test.exs @@ -13,6 +13,7 @@ defmodule JidoTest.ObserveCoverageTest do import ExUnit.CaptureLog alias Jido.Observe + alias JidoTest.Support.TestTracer setup do previous_level = Logger.level() @@ -238,7 +239,7 @@ defmodule JidoTest.ObserveCoverageTest do test "redacts various data types" do Application.put_env(:jido, :observability, redact_sensitive: true) - assert Observe.redact(12345) == "[REDACTED]" + assert Observe.redact(12_345) == "[REDACTED]" assert Observe.redact(%{key: "value"}) == "[REDACTED]" assert Observe.redact([:a, :b, :c]) == "[REDACTED]" assert Observe.redact(nil) == "[REDACTED]" @@ -247,9 +248,9 @@ defmodule JidoTest.ObserveCoverageTest do describe "custom tracer with TestTracer" do setup do - {:ok, _pid} = JidoTest.Support.TestTracer.start_link() - JidoTest.Support.TestTracer.clear() - Application.put_env(:jido, :observability, tracer: JidoTest.Support.TestTracer) + {:ok, _pid} = TestTracer.start_link() + TestTracer.clear() + Application.put_env(:jido, :observability, tracer: TestTracer) :ok end @@ -257,7 +258,7 @@ defmodule JidoTest.ObserveCoverageTest do span_ctx = Observe.start_span([:jido, :tracer, :test], %{key: "val"}) Observe.finish_span(span_ctx, %{extra: 123}) - spans = JidoTest.Support.TestTracer.get_spans() + spans = TestTracer.get_spans() assert Enum.any?(spans, fn {:start, _ref, [:jido, :tracer, :test], %{key: "val"}} -> true @@ -274,7 +275,7 @@ defmodule JidoTest.ObserveCoverageTest do span_ctx = Observe.start_span([:jido, :tracer, :exception], %{}) Observe.finish_span_error(span_ctx, :error, :test_error, []) - spans = JidoTest.Support.TestTracer.get_spans() + spans = TestTracer.get_spans() assert Enum.any?(spans, fn {:exception, _ref, :error, :test_error, []} -> true diff --git a/test/jido/skill/routes_test.exs b/test/jido/skill/routes_test.exs index c6da269..80a894a 100644 --- a/test/jido/skill/routes_test.exs +++ b/test/jido/skill/routes_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.Skill.RoutesTest do use ExUnit.Case, async: true - alias Jido.Skill.Routes alias Jido.Skill.Instance + alias Jido.Skill.Routes defmodule TestAction1 do @moduledoc false diff --git a/test/jido/skill/schedules_test.exs b/test/jido/skill/schedules_test.exs index dbed3e8..ef0bf72 100644 --- a/test/jido/skill/schedules_test.exs +++ b/test/jido/skill/schedules_test.exs @@ -1,8 +1,9 @@ defmodule JidoTest.Skill.SchedulesTest do use ExUnit.Case, async: true - alias Jido.Skill.Schedules alias Jido.Skill.Instance + alias Jido.Skill.Routes + alias Jido.Skill.Schedules defmodule RefreshTokenAction do @moduledoc false @@ -168,7 +169,7 @@ defmodule JidoTest.Skill.SchedulesTest do describe "schedule_route_priority/0" do test "returns a negative priority lower than default skill routes" do priority = Schedules.schedule_route_priority() - default_priority = Jido.Skill.Routes.default_priority() + default_priority = Routes.default_priority() assert priority < default_priority assert priority == -20 diff --git a/test/jido/telemetry_test.exs b/test/jido/telemetry_test.exs index 37490fa..2635beb 100644 --- a/test/jido/telemetry_test.exs +++ b/test/jido/telemetry_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.TelemetryTest do use ExUnit.Case, async: false - alias Jido.Telemetry alias Jido.Agent + alias Jido.Telemetry defmodule TestAgent do @moduledoc false diff --git a/test/jido/tracing/trace_test.exs b/test/jido/tracing/trace_test.exs index 8b7ae76..17acb81 100644 --- a/test/jido/tracing/trace_test.exs +++ b/test/jido/tracing/trace_test.exs @@ -1,8 +1,8 @@ defmodule JidoTest.Tracing.TraceTest do use ExUnit.Case, async: true - alias Jido.Tracing.Trace alias Jido.Signal + alias Jido.Tracing.Trace describe "new_root/0" do test "creates trace with fresh trace_id and span_id" do diff --git a/test/support/eventually.ex b/test/support/eventually.ex index f75e662..af92dad 100644 --- a/test/support/eventually.ex +++ b/test/support/eventually.ex @@ -65,16 +65,18 @@ defmodule JidoTest.Eventually do """ def eventually_state(pid, fun, opts \\ []) do eventually( - fn -> - case Jido.AgentServer.state(pid) do - {:ok, state} -> if fun.(state), do: state, else: false - _ -> false - end - end, + fn -> check_state(pid, fun) end, opts ) end + defp check_state(pid, fun) do + case Jido.AgentServer.state(pid) do + {:ok, state} -> if fun.(state), do: state, else: false + _ -> false + end + end + @doc """ Assert-style wrapper for eventually. diff --git a/test/support/test_agents.ex b/test/support/test_agents.ex index ab23f26..12deeef 100644 --- a/test/support/test_agents.ex +++ b/test/support/test_agents.ex @@ -81,6 +81,8 @@ defmodule JidoTest.TestAgents do @moduledoc false @behaviour Jido.Agent.Strategy + alias Jido.Agent.Strategy.Direct + @impl true def init(agent, _ctx), do: {agent, []} @@ -91,7 +93,7 @@ defmodule JidoTest.TestAgents do def cmd(agent, action, ctx) do count = Map.get(agent.state, :strategy_count, 0) agent = %{agent | state: Map.put(agent.state, :strategy_count, count + 1)} - Jido.Agent.Strategy.Direct.cmd(agent, action, ctx) + Direct.cmd(agent, action, ctx) end end