Packages

Composable multi-agent workflows and LLM orchestrators with human-in-the-loop, checkpointing, and OpenTelemetry observability

Current section

Files

Jump to
jido_composer usage-rules.md
Raw

usage-rules.md

# Jido Composer Usage Rules
## Intent
Build composable agent topologies via two nestable patterns: deterministic
Workflow (FSM) and dynamic Orchestrator (LLM tool-use loop). Human-in-the-Loop
(HumanNode gates, tool approval, generalized suspension) and durable persistence
(checkpoint/thaw/resume across process boundaries) are first-class concerns. The
uniform Node interface (`context → context`) guarantees any node type composes
with any other at any depth.
## Core Contracts
- All nodes implement `context → context` (endomorphism monoid over maps,
composed via Kleisli arrows).
- Node types: **ActionNode** (wraps `Jido.Action`), **AgentNode** (wraps
`Jido.Agent`), **FanOutNode** (parallel branches), **HumanNode** (suspend for
human input), **DynamicAgentNode** (assembles sub-agents from skills at
runtime).
- Nodes return `{:ok, context}`, `{:ok, context, outcome_atom}`, or
`{:error, reason}`. HumanNode returns `{:ok, context, :suspend}`.
- Context layers: each node's output merges under its key via deep merge. Access
upstream results with `get_in(params, [:node_key, :field])`.
- Directives describe side effects: `Suspend`, `SuspendForHuman`,
`FanOutBranch`, `CheckpointAndStop`.
## HITL & Persistence
- Suspension reasons: `:human_input_required`, `:approval_required`, or custom
atoms.
- `ApprovalRequest`: serializable struct with unique `id`, `tool_call`,
`context_snapshot`. `ApprovalResponse`: `approved | rejected | modified` with
optional `modifications`.
- Checkpoint: `Checkpoint.save/2` serializes state. `ChildRef` replaces live
PIDs for safe serialization.
- Resume: `Resume.resume/2` thaws from checkpoint. Top-down: parent resumes,
then re-attaches children.
- Parent isolation: parent doesn't know child is paused. Rejection is
internalized within child.
## Workflow Patterns
- DSL: `use Jido.Composer.Workflow` with `name`, `nodes` (map of atom → module),
`transitions` (map of `{state, outcome} => next_state`).
- Transitions are exhaustive; use `{:_, :error} => :failed` as catch-all.
- Custom outcomes: nodes return `{:ok, ctx, :custom_outcome}` to branch.
Transition map must cover all possible outcomes.
- FanOutNode: `fork_fns` returns list of `{branch_key, fun}` pairs. Results
merge under each branch key.
- HumanNode: always returns `:suspend` outcome. Pair with `SuspendForHuman`
directive for approval gates.
- Terminal states: `:done` and `:failed` are convention defaults (with `:done`
as success state). Custom `terminal_states` require pairing with
`success_states`; providing one without the other is a compile error.
## Orchestrator Patterns
- DSL: `use Jido.Composer.Orchestrator` with `name`, `description`, `tools`
(list of action/agent modules).
- `query_sync/2` drives a ReAct loop: LLM picks tools → execute → feed results
back → repeat until termination.
- `termination_tool`: a `Jido.Action` module whose schema defines structured
output. LLM calls it as a regular tool to emit the final answer.
- Tool wrapping: modules listed in `tools` are auto-converted to LLM tool
descriptions via `AgentTool`.
- Approval gates: per-tool `requires_approval: true` + `approval_policy`
function. Gated tool calls emit `SuspendForHuman`.
- Streaming: `stream: true` uses Finch directly, bypassing Req plugs. Disable
streaming when using cassette/stub plugs for testing.
- LLM config: DSL supports `temperature`, `max_tokens`, `stream`,
`termination_tool`, `llm_opts`.
- **Runtime configuration**: `configure/2` overrides strategy state after
`new/0` but before `query_sync/3`. Accepts `:system_prompt`, `:nodes`,
`:model`, `:temperature`, `:max_tokens`, `:req_options`, `:conversation`. The
`:nodes` override rebuilds tools/name_atoms/schema_keys internally and handles
termination tool dedup.
- **Read accessors**: `get_action_modules/1` returns current node modules;
`get_termination_module/1` returns the termination tool module. Use for
read-filter-write patterns (RBAC).
## Skills & Dynamic Assembly
- **Skill**: pure-data struct (`name`, `description`, `prompt_fragment`,
`tools`). Packages reusable capabilities without defining modules.
- `Skill.assemble/2`: takes a list of skills + options (`:base_prompt`,
`:model`, `:max_iterations`, `:temperature`, `:max_tokens`, `:req_options`),
composes prompt fragments, deduplicates tools, returns a configured
`BaseOrchestrator` agent ready for `query_sync`.
- **DynamicAgentNode**: Node that wraps `Skill.assemble/2` + `query_sync/3`.
Parent LLM selects skills by name via `skills: ["math", "data"]` and provides
a `task` string. The node looks up skills from its registry, assembles a
sub-agent, runs it, and returns the result.
- Inject DynamicAgentNode via `configure(agent, nodes: [dynamic_node])` — the
`build_nodes/1` catch-all handles custom node structs via
`Node.dispatch_name/1`.
- Use direct `Skill.assemble/2` when you want ad-hoc agents without a parent.
Use DynamicAgentNode when the parent LLM should decide which skills to combine
per query.
## Composition
- Any node can be another Workflow or Orchestrator (arbitrary nesting).
- AgentNode wraps a `Jido.Agent` as a node — the child runs its own strategy
internally.
- **Jido.AI agents** (`use Jido.AI.Agent`) are auto-detected via `ask_sync/3`
and work as first-class nodes. Composer spawns a temporary AgentServer,
queries it, and shuts it down. Requires the Jido supervision tree to be
running.
- When used as orchestrator tools, Jido.AI agents expose `{"query": "string"}`
schema (not internal state fields).
- Context flows top-down; child results merge into parent context under the node
key.
- FanOutNode `fork_fns` receive the current context and return branch-specific
params.
- Control spectrum: Workflow only (fully deterministic) → Workflow + HumanNode →
Workflow containing Orchestrator → Orchestrator containing Workflow →
Orchestrator only (fully adaptive).
## Testing
- **ReqCassette** for e2e tests with recorded API responses. Never hand-craft
cassettes; delete and re-record with `RECORD_CASSETTES=true mix test`.
- **LLMStub direct mode** (`LLMStub.setup/1` + `LLMStub.execute/1`):
process-dictionary queue for strategy tests.
- **LLMStub plug mode** (`LLMStub.setup_req_stub/2`): Req.Test.stub-backed queue
for DSL `query_sync` tests.
- LLMAction retries once by default — error stubs need 2+ responses to cover the
retry.
- Disable streaming (`stream: false`) when cassette/stub plug is active (Finch
bypasses Req plugs).
- Propagate `req_options` for plug injection: LLMAction passes them as
`req_http_options` to ReqLLM.
## Observability
- `config :jido, :observability, tracer: AgentObs.JidoTracer` enables
OpenTelemetry tracing.
- AgentObs bridges Jido.Observe spans to OTel via `AgentObs.Handlers.Phoenix`.
- Span hierarchy: AGENT > CHAIN (iteration) > LLM + TOOL. Nested agents parent
under the outer TOOL span.
## Avoid
- Calling LLM APIs directly; use `Orchestrator` + `LLMAction` which handles tool
conversion and retries.
- Embedding runtime side effects in node logic; emit directives instead.
- Using `String.to_atom/1` on untrusted input (node keys, outcomes).
- Assuming streaming works with test plugs; always set `stream: false` in
stub/cassette tests.
- Skipping `mix precommit` before commits.
## References
- `README.md`
- `guides/`
- https://hexdocs.pm/jido_composer
- https://hexdocs.pm/usage_rules/readme.html#usage-rules