Jido Autonomous Agent Framework

repository·main·Indexed 23 days ago

https://github.com/agentjido/jido

An autonomous agent framework for Elixir designed for building complex workflows and multi-agent systems. Built on top of OTP, Jido provides a formalized pattern for agent state management, effect handling via directives, and orchestration. It utilizes an immutable agent architecture where state transitions are explicit, employing Agents, Actions, Signals, and Directives to separate decision logic from external effects.

Tokens
78.4K
Snippets
195
Records
319
Agent score
82%

What's inside Jido

  1. Pod model constraints and scope

    main

    The current Pod implementation follows these design constraints:

    • Topology: Supports predefined topologies and live add/remove mutation for running pods.
    • Ownership: Supports hierarchical ownership for both kind: :agent and kind: :pod nodes.
    • Root: The pod manager acts as the durable root.
    • Runtime: Assumes a single-node runtime.
    • Limitations:
      • No recursive pod ancestry (a pod cannot expand back into itself).
      • No pod-local signal bus.
      • No separate pod instance manager.
      • No standalone link mutation.
      • No reparenting of surviving nodes.
  2. What is a Signal in Jido?

    main
    Signals are typed, CloudEvents-style messages used for standardized communication between agents in the Jido system. They provide a consistent envelope for inter-agent communication, enabling type-based routing and built-in traceability through correlation and causation tracking.
  3. What is a Jido Pod?

    main

    A Jido.Pod is the simplest durable topology layer in Jido. It is an ordinary Jido.Agent module that acts as a durable manager for a topology snapshot.

    Key characteristics:

    • Topology: Represented by %Jido.Pod.Topology{}. It is pure data.
    • Members: Member nodes are durable collaborators acquired via standard Jido.Agent.InstanceManager registries.
    • Runtime: Pods do not use a separate runtime manager; they use the existing Jido.Agent.InstanceManager for the pod agent itself.
    • Plugin: A singleton plugin is automatically mounted under the :__pod__ state key to manage the topology snapshot and advertise the :pod capability.
  4. What is a Sensor and how does it work?

    main

    A Sensor is a module that transforms external world events into Jido.Signals that agents can process. The data flow is: External World → Sensor → Signal → Agent.

    Sensors are pure modules that define two primary callbacks:

    1. init/2: Sets up the initial state.
    2. handle_event/2: Transforms incoming events into signals.

    The actual execution and lifecycle management are handled by the Jido.Sensor.Runtime GenServer.

  5. When to use Sensors in Jido

    main
    Sensors should be used when an agent needs to react to changes in the external world or external systems. They act as the bridge between the Jido runtime and external environments, converting external state changes into Jido signals that can be processed by agents and orchestrators.
  6. Use Worker Pools for efficient agent execution

    main

    Worker pools allow you to run concurrent work by checking out pre-warmed agents from a pool instead of spawning a new agent per request. This is significantly faster (sub-millisecond checkout) and prevents the overhead of expensive agent initialization.

    When to use pools:

    • Agent initialization is expensive (e.g., loading ML models, establishing database connections).
    • You need bounded concurrency for resource-limited operations.
    • You want consistent latency and want to avoid 'cold starts'.

    When to use spawn-per-request:

    • Agents require strict per-request state isolation.
    • Initialization is cheap.
    • Request volume is highly unpredictable and bursty.
    # Before: spawn a new agent per request (expensive initialization)
    {:ok, pid} = Jido.start_agent(jido, SearchAgent)
    result = AgentServer.call(pid, signal)
    Jido.stop_agent(jido, agent_id)  # Teardown overhead
    
    # After: checkout from pre-warmed pool (sub-millisecond)
    {:ok, result} = Jido.Agent.WorkerPool.call(MyApp.Jido, :search, signal)
  7. Tune observability settings per Jido instance

    main

    Jido allows per-instance observability tuning. This is useful when you want different verbosity or redaction settings for internal vs. public-facing Jido instances. Settings resolve in the following order:

    1. Debug override (MyApp.Jido.debug(:on))
    2. Instance configuration
    3. Global configuration
    4. Default values.

    Common configuration options:

    • telemetry: Controls log level (e.g., log_level: :debug) and argument verbosity (log_args: :full, :keys_only, or :none).
    • observability: Controls debug_events (e.g., :off, :all) and redact_sensitive (replaces sensitive data with [REDACTED]).
    • tracer: Specifies a custom OpenTelemetry tracer (e.g., MyApp.OtelTracer).

    Note on log_args for Jido.Exec calls:

    • :full: Keeps verbose action logs and emits [:jido, :action, ...] spans.
    • :keys_only or :none: Suppresses noisy action logs and dependency action spans.
    # Example of per-instance configuration
    config :my_app, MyApp.PublicJido,
      telemetry: [log_level: :info],
      observability: [debug_events: :off, redact_sensitive: true]
    
    config :my_app, MyApp.InternalJido,
      telemetry: [log_level: :debug, log_args: :full],
      observability: [debug_events: :all, redact_sensitive: false, tracer: MyApp.OtelTracer]
  8. Understand Jido agent lifecycle states and parent references

    main

    Jido tracks parent-child relationships logically. An agent's state contains specific keys to track current and former parents:

    Statestate.parent / agent.state.__parent__state.orphaned_from / agent.state.__orphaned_from__
    Standalonenilnil
    Attached Child%ParentRef{}nil
    Orphaned Childnil%ParentRef{}

    Important Behaviors:

    • Routing: Current parent routing always uses state.parent / agent.state.__parent__.
    • Provenance: Former-parent information is stored in state.orphaned_from / agent.state.__orphaned_from__.
    • Communication: Directive.emit_to_parent/3 returns nil if the agent is orphaned. It only works for currently attached children.
  9. How Directive.Error works for directive-based processing

    main

    The Directive.Error struct wraps Jido.Error instances for directive-based processing. When an error occurs during command handling (via cmd/2), agents emit this directive.

    The context field in a Directive.Error indicates where the error originated:

    • :normalize: Error during signal normalization
    • :instruction: Error during action instruction execution
    • :fsm_transition: Error during FSM state transition
    • :routing: Error during signal routing
    • :plugin_handle_signal: Error in plugin signal handler
    • :plugin_prepare_signal: Error while verifying, decrypting, or preparing a signal
    • :plugin_prepare_action: Error while authorizing a resolved action
    • :plugin_prepare_emit: Error while preparing an emitted signal for dispatch
    alias Jido.Agent.Directive
    
    # Create an error directive
    Directive.error(Jido.Error.validation_error("Invalid input"))
    
    # With context (where the error occurred)
    Directive.error(error, :normalize)
    Directive.error(error, :instruction)
  10. Understand Jido configuration resolution order

    main

    Observability settings (like telemetry and debug levels) are resolved using the following hierarchy. The first applicable setting wins:

    1. Jido.Debug runtime override: Set via MyApp.Jido.debug(...) (uses persistent_term).
    2. Per-instance app config: Configured specifically for a Jido instance (e.g., config :my_app, MyApp.PublicJido, ...).
    3. Global app config: General Jido configuration (e.g., config :jido, :telemetry).
    4. Hardcoded default.

    Note: If the instance is nil, steps 1 and 2 are skipped.

  11. Distinguish between Agent and AgentServer

    main

    In Jido, responsibilities are split between a data structure and a process:

    ConceptWhat It IsResponsibility
    AgentImmutable struct + moduleDefines schema, handles cmd/2, pure decision logic
    AgentServerGenServer processHolds agent state, executes directives, routes signals

    An Agent defines how state changes in response to actions, while the AgentServer is the OTP process that manages the lifecycle and execution of those changes.

  12. Configure multiple Jido instances

    main

    For multi-tenant applications or isolation, you can define multiple Jido instances by creating separate modules for each. Each instance can have its own independent configuration.

    If you require logical multi-tenancy within a single shared Jido instance, use partition as the boundary. This allows you to keep registry identity, persistence, and telemetry isolated per tenant without the overhead of separate BEAM supervision trees.

    # Define separate instances
    defmodule MyApp.TenantA.Jido do
      use Jido, otp_app: :my_app
    end
    
    # Configure them independently
    config :my_app, MyApp.TenantA.Jido, max_tasks: 500
    config :my_app, MyApp.TenantB.Jido, max_tasks: 1000
    
    # Logical multi-tenancy within one instance
    {:ok, workspace_pid} = 
      Jido.Pod.get(MyApp.Jido.WorkspacePods, "workspace-123", partition: :tenant_alpha)