You need a reliable way to compose mountable capabilities (plugins) with executable primitives (actions) without coupling to strategy internals.
After this guide, you can choose the right extension surface for each requirement.
Pick Your Extension Surface
Jido AI gives you three extension surfaces — plugins, actions, and strategies. Each solves a different problem. Start with the scenario that matches yours.
Scenario A: You're building a reusable chat capability
You want any agent in your system to gain chat powers by adding one line to its plugin list.
Reach for a Plugin.
defmodule MyApp.Assistant do
use Jido.Agent,
name: "assistant",
plugins: [
{Jido.AI.Plugins.Chat, %{default_model: :capable, auto_execute: true}}
]
endPlugins give you lifecycle hooks (mount/2), capability-level defaults, and stable signal contracts (chat.message, chat.simple, etc.) that the rest of your app can rely on. When you need the same capability across many agents, plugins are the answer.
Scenario B: You're adding LLM calls to a background job
You have an Oban worker or a one-off Mix task that needs to call an LLM. You don't need an agent, lifecycle hooks, or signal routing — you just need to run an action and get a result.
Use an Action directly via Jido.Exec.
{:ok, result} =
Jido.Exec.run(Jido.AI.Actions.Chat.SimpleChat, %{
model: :fast,
prompt: "Summarize this support ticket."
})Actions are the lowest-level primitive. They validate params, call the LLM, and return {:ok, result} or {:error, reason}. No plugin state, no agent process required.
Scenario C: You want a custom multi-step reasoning approach
You need chain-of-thought, tree-of-thoughts, or your own bespoke reasoning loop that orchestrates multiple LLM calls with intermediate evaluation.
Configure a Strategy (and optionally wrap it in a Reasoning plugin).
defmodule MyApp.Analyst do
use Jido.AI.Agent,
name: "analyst",
strategy: {Jido.AI.Reasoning.ReAct.Strategy, [tools: [MyApp.Tools.Search], model: :reasoning]},
plugins: [
{Jido.AI.Plugins.Reasoning.ChainOfThought, %{default_model: :reasoning}}
]
endStrategies own the multi-step execution loop. Reasoning plugins provide signal routing and defaults on top of strategies so you can trigger them via reasoning.cot.run and friends.
Decision Table
| You need to… | Use a… | Example |
|---|---|---|
| Add a reusable capability to multiple agents | Plugin | Jido.AI.Plugins.Chat |
| Make a single LLM call in a script or job | Action | Jido.Exec.run(SimpleChat, params) |
| Orchestrate multi-step reasoning | Strategy | Jido.AI.Reasoning.ReAct.Strategy |
| Expose a capability via stable signal contract | Plugin | chat.message, reasoning.cot.run |
| Compose actions without agent lifecycle | Action | Chain actions with Jido.Exec |
| Customize reasoning defaults per agent | Plugin | Jido.AI.Plugins.Reasoning.ChainOfThought |
The rest of this guide covers the contracts and defaults for each surface.
Public Plugin Surface (v3)
Public capability plugins:
Jido.AI.Plugins.ChatJido.AI.Plugins.PlanningJido.AI.Plugins.Reasoning.ChainOfDraftJido.AI.Plugins.Reasoning.ChainOfThoughtJido.AI.Plugins.Reasoning.AlgorithmOfThoughtsJido.AI.Plugins.Reasoning.TreeOfThoughtsJido.AI.Plugins.Reasoning.GraphOfThoughtsJido.AI.Plugins.Reasoning.TRMJido.AI.Plugins.Reasoning.Adaptive
Internal runtime plugin:
Jido.AI.Plugins.TaskSupervisor(infrastructure, not a public capability recommendation)
Removed public plugins:
Jido.AI.Plugins.LLMJido.AI.Plugins.ToolCallingJido.AI.Plugins.Reasoning
Compose In Agent Definition
defmodule MyApp.Assistant do
use Jido.Agent,
name: "assistant",
plugins: [
{Jido.AI.Plugins.Chat, %{default_model: :capable, auto_execute: true}},
{Jido.AI.Plugins.Planning, %{}},
{Jido.AI.Plugins.Reasoning.ChainOfThought, %{default_model: :reasoning}}
]
endPlugin Signal Contracts
Jido.AI.Plugins.Chatchat.message-> tool-aware chat (CallWithTools) with auto-execute defaulting totruechat.simple-> direct chat generationchat.complete-> completion convenience pathchat.embed-> embedding generationchat.generate_object-> schema-constrained structured outputchat.execute_tool-> direct tool execution by tool namechat.list_tools-> tool inventory for the active chat capability
Jido.AI.Plugins.Planningplanning.plan-> structured plan generation (Jido.AI.Actions.Planning.Plan)planning.decompose-> goal decomposition (Jido.AI.Actions.Planning.Decompose)planning.prioritize-> task ordering (Jido.AI.Actions.Planning.Prioritize)
Jido.AI.Plugins.Reasoning.*reasoning.cod.runreasoning.cot.runreasoning.aot.runreasoning.tot.runreasoning.got.runreasoning.trm.runreasoning.adaptive.run- All route to
Jido.AI.Actions.Reasoning.RunStrategywith fixed strategy identity.
TaskSupervisor Internal Runtime Contract
Jido.AI.Plugins.TaskSupervisor is internal runtime infrastructure enabled by
default via Jido.AI.PluginStack.default_plugins/1. It is not part of the
public capability plugin surface.
State key contract:
- Plugin state key is
:__task_supervisor_skill__ mount/2stores%{supervisor: pid}underagent.state.__task_supervisor_skill__- Runtime directives resolve this supervisor via
Jido.AI.Directive.Helpers.get_task_supervisor/1
Lifecycle and cleanup contract:
mount/2starts an anonymousTask.Supervisorlinked to the mounting agent process- Each agent instance receives its own supervisor PID
- When the owning agent process terminates, the linked supervisor terminates automatically
ModelRouting Runtime Contract
Jido.AI.Plugins.ModelRouting is a cross-cutting runtime plugin (enabled by
default in Jido.AI.Agent) that assigns model aliases by signal type when the
caller does not explicitly provide one.
Route precedence:
- Exact route keys win first (
"chat.simple") - Wildcard route keys are fallback (
"reasoning.*.run") - Explicit payload model (
:modelor"model") bypasses plugin routing
Wildcard behavior:
*matches exactly one dot-delimited segment"reasoning.*.run"matches"reasoning.cot.run""reasoning.*.run"does not match"reasoning.cot.worker.run"
Production-style config shape (using Jido.Agent so you can mount/configure
this plugin directly):
defmodule MyApp.RoutedAssistant do
use Jido.Agent,
name: "routed_assistant",
strategy: {Jido.AI.Reasoning.ReAct.Strategy, [tools: [MyApp.Tools.Search], model: :fast]},
plugins: [
{Jido.AI.Plugins.ModelRouting,
%{
routes: %{
"chat.message" => :capable,
"chat.simple" => :fast,
"chat.generate_object" => :thinking,
"reasoning.*.run" => :reasoning
}
}}
]
endIf you are using Jido.AI.Agent, ModelRouting is already mounted by default.
Do not add a second ModelRouting plugin instance (duplicate state key). Use
Jido.Agent when you need custom ModelRouting plugin config.
Policy Runtime Contract
Jido.AI.Plugins.Policy is a cross-cutting runtime plugin (enabled by default
in Jido.AI.Agent) that hardens request/query inputs and normalizes outbound
result/delta envelopes.
Enforce mode behavior:
mode: :enforcerewrites violating request/query signals toai.request.errormode: :monitorkeeps request/query signals unchanged while still observingblock_on_validation_error: truecontrols whether validation failures block
Rewrite semantics:
- Enforceable request/query signal types include
chat.*,ai.*.query, andreasoning.*.run - Prompt/query fields are validated via
Jido.AI.Validation.validate_prompt/1 - Violations rewrite to
ai.request.errorwithreason: :policy_violation
Normalization and sanitization:
ai.llm.responseandai.tool.resultnormalize malformeddata.resultto{:error, %{code: :malformed_result, ...}, []}ai.llm.deltastrips control bytes fromdata.deltaand truncates tomax_delta_chars
Policy hardening config shape (using Jido.Agent so you can mount/configure
this plugin directly):
defmodule MyApp.PolicyHardenedAssistant do
use Jido.Agent,
name: "policy_hardened_assistant",
strategy: {Jido.AI.Reasoning.ReAct.Strategy, [tools: [MyApp.Tools.Search], model: :fast]},
plugins: [
{Jido.AI.Plugins.Policy,
%{
mode: :enforce,
block_on_validation_error: true,
max_delta_chars: 2_000
}}
]
endIf you are using Jido.AI.Agent, Policy is already mounted by default. Do
not add a second Policy plugin instance (duplicate state key). Use
Jido.Agent when you need custom Policy plugin config.
Retrieval Runtime Contract
Jido.AI.Plugins.Retrieval is an optional cross-cutting runtime plugin that
enriches chat.message and reasoning.*.run prompts with in-process memory.
Retrieval enrichment lifecycle:
- Read mounted retrieval state (
enabled,namespace,top_k,max_snippet_chars) - Skip enrichment when plugin
enabled: false - Skip enrichment when payload sets
disable_retrieval: true - Resolve query text from
promptorquery - Recall top-k snippets from namespace memory
- Rewrite prompt with relevant memory block and attach
data.retrievalmetadata
Opt-out behavior:
- Global opt-out: mount plugin with
enabled: false - Per-request opt-out: set
disable_retrieval: truein signal payload
Namespace behavior:
namespaceconfig is used for both enrichment and retrieval action routes (retrieval.upsert,retrieval.recall,retrieval.clear)- If omitted, namespace falls back to agent id, then
"default"
Retrieval action route contracts:
retrieval.upsert->Jido.AI.Actions.Retrieval.UpsertMemory- Required params:
text - Optional params:
id,metadata,namespace - Returns
%{retrieval: %{namespace, last_upsert}}
- Required params:
retrieval.recall->Jido.AI.Actions.Retrieval.RecallMemory- Required params:
query - Optional params:
top_k(default3),namespace - Returns
%{retrieval: %{namespace, query, memories, count}}
- Required params:
retrieval.clear->Jido.AI.Actions.Retrieval.ClearMemory- Required params: none
- Optional params:
namespace - Returns
%{retrieval: %{namespace, cleared}}
Retrieval plugin config shape:
defmodule MyApp.RetrievalEnabledAssistant do
use Jido.AI.Agent,
name: "retrieval_enabled_assistant",
plugins: [
{Jido.AI.Plugins.Retrieval,
%{
enabled: true,
namespace: "weather_ops",
top_k: 3,
max_snippet_chars: 280
}}
]
end
signal =
Jido.Signal.new!(
"chat.message",
%{prompt: "Should I bike to work in Seattle tomorrow?", disable_retrieval: true},
source: "/cli"
)Quota Runtime Contract
Jido.AI.Plugins.Quota is an optional cross-cutting runtime plugin that
tracks rolling request/token usage and rejects over-budget requests.
Quota state keys:
enabledtoggles budget enforcementscopeselects the quota namespacewindow_msdefines rolling budget window durationmax_requestssets per-window request budget (nildisables request cap)max_total_tokenssets per-window token budget (nildisables token cap)error_messagesets rejection message for blocked requests
Quota action route contracts:
quota.status->Jido.AI.Actions.Quota.GetStatus- Required params: none
- Optional params:
scope - Returns
%{quota: %{scope, window_ms, usage, limits, remaining, over_budget?}}
quota.reset->Jido.AI.Actions.Quota.Reset- Required params: none
- Optional params:
scope - Returns
%{quota: %{scope, reset}}
Scope resolution precedence for quota actions:
- Explicit action param (
scope) context[:plugin_state][:quota][:scope]context[:state][:quota][:scope]context[:agent][:id]"default"
GetStatus context defaults for limits/window:
window_ms:context[:plugin_state][:quota][:window_ms]->context[:state][:quota][:window_ms]->60_000max_requests:context[:plugin_state][:quota][:max_requests]->context[:state][:quota][:max_requests]->nilmax_total_tokens:context[:plugin_state][:quota][:max_total_tokens]->context[:state][:quota][:max_total_tokens]->nil
Usage accounting contract:
ai.usageincrements counters forrequestsandtotal_tokenstotal_tokensis preferred, with fallback toinput_tokens + output_tokens- Quota status snapshots expose
usage,limits,remaining, andover_budget?
Budget rejection contract:
- Budgeted signal types include
chat.*,ai.*.query, andreasoning.*.run - When quota is exceeded, requests rewrite to
ai.request.error - Rejection payload uses
reason: :quota_exceeded request_idis resolved from request correlation fields when present
Quota plugin config shape:
defmodule MyApp.QuotaGuardedAssistant do
use Jido.AI.Agent,
name: "quota_guarded_assistant",
plugins: [
{Jido.AI.Plugins.Quota,
%{
enabled: true,
scope: "assistant_ops",
window_ms: 60_000,
max_requests: 50,
max_total_tokens: 20_000,
error_message: "quota exceeded for current window"
}}
]
end
signal =
Jido.Signal.new!(
"chat.message",
%{prompt: "Summarize this report in one paragraph.", call_id: "req_123"},
source: "/cli"
)
# Expected rewrite shape when over budget:
# %Jido.Signal{
# type: "ai.request.error",
# data: %{
# request_id: "req_123",
# reason: :quota_exceeded,
# message: "quota exceeded for current window"
# }
# }CoD Plugin Handoff (reasoning.cod.run)
defmodule MyApp.CoDPluginAgent do
use Jido.AI.Agent,
name: "cod_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.ChainOfDraft,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{llm_timeout_ms: 20_000}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.cod.run",
%{
prompt: "Give me a terse plan with one backup.",
strategy: :cot
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.cod.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.ChainOfDraftoverrides payload strategy tostrategy: :cod.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.
CoT Plugin Handoff (reasoning.cot.run)
defmodule MyApp.CoTPluginAgent do
use Jido.AI.Agent,
name: "cot_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.ChainOfThought,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{llm_timeout_ms: 20_000}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.cot.run",
%{
prompt: "Lay out the reasoning steps and one fallback.",
strategy: :cod
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.cot.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.ChainOfThoughtoverrides payload strategy tostrategy: :cot.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.
AoT Plugin Handoff (reasoning.aot.run)
defmodule MyApp.AoTPluginAgent do
use Jido.AI.Agent,
name: "aot_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.AlgorithmOfThoughts,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{profile: :standard, search_style: :dfs, llm_timeout_ms: 20_000}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.aot.run",
%{
prompt: "Solve this with algorithmic steps and one fallback.",
strategy: :cot,
options: %{profile: :long, require_explicit_answer: true}
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.aot.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.AlgorithmOfThoughtsoverrides payload strategy tostrategy: :aot.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.
ToT Plugin Handoff (reasoning.tot.run)
defmodule MyApp.ToTPluginAgent do
use Jido.AI.Agent,
name: "tot_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.TreeOfThoughts,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{branching_factor: 3, max_depth: 4, traversal_strategy: :best_first}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.tot.run",
%{
prompt: "Explore three weather-safe plans with tradeoffs.",
strategy: :cot,
options: %{branching_factor: 4, max_depth: 5}
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.tot.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.TreeOfThoughtsoverrides payload strategy tostrategy: :tot.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.- ToT option keys include
branching_factor,max_depth,traversal_strategy,generation_prompt, andevaluation_prompt.
GoT Plugin Handoff (reasoning.got.run)
defmodule MyApp.GoTPluginAgent do
use Jido.AI.Agent,
name: "got_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.GraphOfThoughts,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{max_nodes: 20, max_depth: 5, aggregation_strategy: :synthesis}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.got.run",
%{
prompt: "Compare three weather scenarios and synthesize one recommendation.",
strategy: :cot,
options: %{max_nodes: 25, aggregation_strategy: :weighted}
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.got.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.GraphOfThoughtsoverrides payload strategy tostrategy: :got.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.- GoT option keys include
max_nodes,max_depth,aggregation_strategy,generation_prompt,connection_prompt, andaggregation_prompt.
TRM Plugin Handoff (reasoning.trm.run)
defmodule MyApp.TRMPluginAgent do
use Jido.AI.Agent,
name: "trm_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.TRM,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{max_supervision_steps: 6, act_threshold: 0.92}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.trm.run",
%{
prompt: "Recursively improve this emergency plan and stop when confidence is high.",
strategy: :cot,
options: %{max_supervision_steps: 7, act_threshold: 0.95}
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.trm.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.TRMoverrides payload strategy tostrategy: :trm.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.- TRM option keys include
max_supervision_stepsandact_threshold.
Adaptive Plugin Handoff (reasoning.adaptive.run)
defmodule MyApp.AdaptivePluginAgent do
use Jido.AI.Agent,
name: "adaptive_plugin_agent",
plugins: [
{Jido.AI.Plugins.Reasoning.Adaptive,
%{
default_model: :reasoning,
timeout: 30_000,
options: %{
default_strategy: :react,
available_strategies: [:cod, :cot, :react, :tot, :got, :trm, :aot],
complexity_thresholds: %{simple: 0.3, complex: 0.7}
}
}}
]
end
signal =
Jido.Signal.new!(
"reasoning.adaptive.run",
%{
prompt: "Pick the best strategy and produce a weather-safe commute with one backup.",
strategy: :cot,
options: %{default_strategy: :tot}
},
source: "/cli"
)Execution handoff:
- Route dispatch maps
reasoning.adaptive.runtoJido.AI.Actions.Reasoning.RunStrategy. Jido.AI.Plugins.Reasoning.Adaptiveoverrides payload strategy tostrategy: :adaptive.RunStrategyapplies plugin defaults (default_model,timeout,options) from plugin state when omitted by caller params.- Adaptive option keys include
default_strategy,available_strategies, andcomplexity_thresholds.
Chat Plugin Defaults Contract
Jido.AI.Plugins.Chat mounts the following defaults unless overridden in plugin config:
default_model: :capabledefault_max_tokens: 4096default_temperature: 0.7default_system_prompt: nilauto_execute: truemax_turns: 10tool_policy: :allow_alltools: %{}(normalized from configured tool modules)available_tools: []
Tool Registry Precedence Contract
Tool-calling actions (CallWithTools, ExecuteTool, ListTools) resolve tool maps with this precedence:
context[:tools]context[:tool_calling][:tools]context[:chat][:tools]context[:state][:tool_calling][:tools]context[:state][:chat][:tools]context[:agent][:state][:tool_calling][:tools]context[:agent][:state][:chat][:tools]context[:plugin_state][:tool_calling][:tools]context[:plugin_state][:chat][:tools]
First non-nil tool map wins. This keeps direct Jido.Exec action calls and plugin-routed calls deterministic.
ListTools security filtering defaults:
- sensitive names are excluded by default
include_sensitive: truedisables sensitive-name filteringallowed_tools: [...]applies an allowlist after sensitive filtering unlessinclude_sensitive: trueis set
Planning Plugin Defaults Contract
Jido.AI.Plugins.Planning mounts the following defaults unless overridden in plugin config:
default_model: :planningdefault_max_tokens: 4096default_temperature: 0.7
Planning actions consume these plugin defaults when the caller omits those params. Action-specific fields remain action-owned:
Plan:goal, optionalconstraints/resources, optionalmax_stepsDecompose:goal, optionalmax_depth, optionalcontextPrioritize:tasks, optionalcriteria, optionalcontext
Planning selection guidance:
- Use
Planwhen you need a sequential execution plan from one goal. - Use
Decomposewhen the goal is too large and should be split into hierarchical sub-goals. - Use
Prioritizewhen you already have a task list and need ranked execution order.
Model Routing Plugin Defaults Contract
Jido.AI.Plugins.ModelRouting mounts default routes unless overridden by plugin
config:
"chat.message" => :capable"chat.simple" => :fast"chat.complete" => :fast"chat.embed" => :embedding"chat.generate_object" => :thinking"reasoning.*.run" => :reasoning
Exact routes take precedence over wildcard routes. Wildcards use a
single-segment * matcher between dots.
Reasoning CoT Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.ChainOfThought mounts the following defaults unless overridden in plugin config:
strategy: :cotdefault_model: :reasoningtimeout: 30_000options: %{}
Reasoning AoT Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.AlgorithmOfThoughts mounts the following defaults unless overridden in plugin config:
strategy: :aotdefault_model: :reasoningtimeout: 30_000options: %{}
Reasoning ToT Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.TreeOfThoughts mounts the following defaults unless overridden in plugin config:
strategy: :totdefault_model: :reasoningtimeout: 30_000options: %{}
Reasoning GoT Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.GraphOfThoughts mounts the following defaults unless overridden in plugin config:
strategy: :gotdefault_model: :reasoningtimeout: 30_000options: %{}
Reasoning TRM Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.TRM mounts the following defaults unless overridden in plugin config:
strategy: :trmdefault_model: :reasoningtimeout: 30_000options: %{}
Reasoning Adaptive Plugin Defaults Contract
Jido.AI.Plugins.Reasoning.Adaptive mounts the following defaults unless overridden in plugin config:
strategy: :adaptivedefault_model: :reasoningtimeout: 30_000options: %{}
Action Context Contract (Plugin -> Action)
When plugin-routed actions execute, the action context contract includes:
stateagentplugin_stateprovided_params
Actions should read defaults from explicit params first, then context/plugin state fallback.
Strategy Runtime Compatibility
All built-in reasoning strategies now support module-action fallback execution for non-strategy commands. This means plugin-routed Jido.Action modules execute on strategy agents instead of silently no-oping.
When To Use Plugins vs Actions
Use plugins when:
- capability should be mountable and reusable across many agents
- you need lifecycle hooks and capability-level defaults
- you want stable signal contracts for app/runtime integration
Use actions directly when:
- you are building one-off pipelines or background jobs
- you need low-level composition via
Jido.Exec - you do not need plugin lifecycle behavior
Failure Mode: Defaults Not Being Applied
Symptom:
- execution succeeds but model/tool defaults are ignored
Fix:
- verify plugin state keys and
mount/2shape match plugin schema and action fallback readers - ensure caller does not override defaults with empty explicit params