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/promptstatus- Current status (:pending,:completed,:failed)result- The result when completederror- Error details if failedinserted_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
Functions
@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)
@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}.
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
@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_msis 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-:rawto 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"
)
Extracts request_id from action params, generating one if not present.
Use this in signal routing or action preparation.
Marks a request as failed with an error.
Called when a request encounters an error.
Gets a request by ID from agent state.
Returns nil if not found.
@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.
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}}
@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
@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"
)
@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