Jido.AI.Request (Jido AI v2.2.0)

Copy Markdown View Source

Request tracking for AI agents with per-request isolation and correlation.

This module provides a standardized way to track requests and their results in AI agents, solving the "single-slot overwrite" problem where concurrent requests can overwrite each other's results.

Pattern

Follows the Elixir Task.async/await pattern:

# Async (returns handle for later awaiting)
{:ok, request} = MyAgent.ask(pid, "What is 2+2?")

# Await specific request
{:ok, result} = MyAgent.await(request, timeout: 30_000)

# Or use sync convenience wrapper
{:ok, result} = MyAgent.ask_sync(pid, "What is 2+2?")

Request Struct

The Request struct contains:

  • id - Unique request identifier (UUID)
  • server - The agent server (pid or via tuple)
  • query - The original query/prompt
  • status - Current status (:pending, :completed, :failed)
  • result - The result when completed
  • error - Error details if failed
  • inserted_at - When the request was created

State Schema

Agents using request tracking should include in their state:

requests: %{request_id => Handle.t()}

This module provides helpers for managing this map.

Usage in Agent Macros

defmodule MyAgent do
  use Jido.AI.Agent, ...

  # ask/2 now returns {:ok, Handle.t()}
  def ask(pid, query, opts \\ []) do
    Jido.AI.Request.create_and_send(pid, query, opts,
      signal_type: "ai.react.query",
      source: "/ai/react/agent"
    )
  end

  # await/2 waits for specific request
  def await(request, opts \\ []) do
    Jido.AI.Request.await(request, opts)
  end
end

Summary

Functions

Awaits completion of a specific request.

Awaits multiple requests, returning results in the same order.

Marks a request as completed with a result.

Creates a request, sends the signal, and returns the request handle.

Extracts request_id from action params, generating one if not present.

Marks a request as failed with an error.

Gets a request by ID from agent state.

Gets the result of a request if completed.

Initializes request tracking state fields.

Returns the Zoi schema fields for request tracking.

Synchronously sends a request and waits for the result.

Records a new request in agent state.

Types

server()

@type server() :: pid() | atom() | {:via, module(), term()}

status()

@type status() :: :pending | :completed | :failed | :timeout

Functions

await(handle, opts \\ [])

@spec await(
  Jido.AI.Request.Handle.t(),
  keyword()
) :: {:ok, any()} | {:error, term()}

Awaits completion of a specific request.

Similar to Task.await/2, this blocks until the request completes, fails, or times out.

Options

  • :timeout - How long to wait (default: 30_000ms)

Request.await/2 always uses the internal request paths under state.requests[request_id] for status, result, and error tracking.

Returns

  • {:ok, result} - Request completed successfully
  • {:error, :timeout} - Request didn't complete in time
  • {:error, reason} - Request failed

Examples

{:ok, request} = MyAgent.ask(pid, "question")
{:ok, result} = Request.await(request, timeout: 10_000)

await_many(requests, opts \\ [])

@spec await_many(
  [Jido.AI.Request.Handle.t()],
  keyword()
) :: [ok: any(), error: term()]

Awaits multiple requests, returning results in the same order.

Similar to Task.await_many/2.

Options

  • :timeout - How long to wait for all requests (default: 30_000ms)

Returns

A list of results in the same order as the input requests. Each element is either {:ok, result} or {:error, reason}.

complete_request(agent, request_id, result, opts \\ [])

@spec complete_request(struct(), String.t(), any(), keyword()) :: struct()

Marks a request as completed with a result.

Called in on_after_cmd/3 when a request finishes successfully.

Examples

def on_after_cmd(agent, {:ai_react_start, %{request_id: req_id}}, directives) do
  snap = strategy_snapshot(agent)
  if snap.done? do
    agent = Request.complete_request(agent, req_id, snap.result)
  end
  {:ok, agent, directives}
end

create_and_send(server, query, opts)

@spec create_and_send(server(), Jido.AI.Query.t(), keyword()) ::
  {:ok, Jido.AI.Request.Handle.t()} | {:error, term()}

Creates a request, sends the signal, and returns the request handle.

This is the primary entry point for agents implementing ask/2.

Options

  • :tool_context - Additional context merged with agent's tool_context
  • :tools - ReAct-only request-scoped tool registry override for this run
  • :allowed_tools - ReAct-only request-scoped allowlist of tool names
  • :request_transformer - ReAct-only module implementing per-turn request shaping
  • :max_iterations - ReAct-only request-scoped maximum reasoning iterations
  • :stream_timeout_ms - ReAct-only request-scoped runtime inactivity timeout. :stream_receive_timeout_ms is accepted as a compatibility alias.
  • :req_http_options - Per-request Req HTTP options forwarded to ReAct runtime
  • :llm_opts - Per-request ReqLLM generation options forwarded to ReAct runtime
  • :file_id / :file_ids / :file_reference / :file_references - Uploaded file references appended to the user query as ReqLLM content parts when supported by the ReqLLM version
  • :output - :raw to bypass agent-level structured output for this request, or a request-scoped structured output config
  • :extra_refs - Map of additional refs to attach to the user message thread entry
  • :stream_to - Optional request-scoped runtime event sink, currently {:pid, pid}
  • :request_id - Custom request ID (auto-generated if not provided)

Signal Options (required)

  • :signal_type - The signal type to create (e.g., "ai.react.query")
  • :source - The signal source (e.g., "/ai/react/agent")

Examples

{:ok, request} = Request.create_and_send(pid, "What is 2+2?",
  tool_context: %{actor: user},
  signal_type: "ai.react.query",
  source: "/ai/react/agent"
)

ensure_request_id(params)

@spec ensure_request_id(map()) :: {String.t(), map()}

Extracts request_id from action params, generating one if not present.

Use this in signal routing or action preparation.

fail_request(agent, request_id, error)

@spec fail_request(struct(), String.t(), any()) :: struct()

Marks a request as failed with an error.

Called when a request encounters an error.

get_request(agent, request_id)

@spec get_request(
  struct(),
  String.t()
) :: map() | nil

Gets a request by ID from agent state.

Returns nil if not found.

get_result(agent, request_id)

@spec get_result(
  struct(),
  String.t()
) :: {:ok, any()} | {:error, any()} | {:pending, map()} | nil

Gets the result of a request if completed.

Returns {:ok, result} if completed, {:error, error} if failed, or {:pending, request} if still in progress.

init_state(state, opts \\ [])

@spec init_state(
  map(),
  keyword()
) :: map()

Initializes request tracking state fields.

Call this when setting up agent state to add the requests map.

Options

  • :max_requests - Maximum requests to keep (default: 100)

Examples

state = Request.init_state(%{})
# => %{requests: %{}, __request_tracking__: %{max_requests: 100}}

schema_fields()

@spec schema_fields() :: map()

Returns the Zoi schema fields for request tracking.

Include this in your agent macro's schema definition.

Example

base_schema_ast = quote do
  Zoi.object(Map.merge(
    %{
      model: Zoi.string() |> Zoi.default("..."),
      # ... other fields
    },
    Jido.AI.Request.schema_fields()
  ))
end

send_and_await(server, query, opts)

@spec send_and_await(server(), Jido.AI.Query.t(), keyword()) ::
  {:ok, any()} | {:error, term()}

Synchronously sends a request and waits for the result.

Convenience wrapper that combines create_and_send/3 and await/2.

Options

All options from create_and_send/3 plus:

  • :timeout - How long to wait (default: 30_000ms)

Examples

{:ok, result} = Request.send_and_await(pid, "What is 2+2?",
  timeout: 10_000,
  signal_type: "ai.react.query",
  source: "/ai/react/agent"
)

start_request(agent, request_id, query, opts \\ [])

@spec start_request(struct(), String.t(), Jido.AI.Query.t(), keyword()) :: struct()

Records a new request in agent state.

Called in on_before_cmd/2 when a request starts.

Examples

def on_before_cmd(agent, {:ai_react_start, %{query: query, request_id: req_id}} = action) do
  agent = Request.start_request(agent, req_id, query)
  {:ok, agent, action}
end