# Request Lifecycle And Concurrency

You need concurrent requests without single-slot state overwrites.

After this guide, you will use request handles (`ask/await`) and collect multiple results safely.

## Core Pattern

`Jido.AI.Request` follows a `Task.async/await` style model:

```elixir
{:ok, req1} = MyApp.MathAgent.ask(pid, "2 + 2")
{:ok, req2} = MyApp.MathAgent.ask(pid, "3 + 3")

{:ok, r1} = MyApp.MathAgent.await(req1)
{:ok, r2} = MyApp.MathAgent.await(req2)
```

Per-request ReAct overrides travel with the request handle:

```elixir
{:ok, req3} =
  MyApp.MathAgent.ask(pid, "Search only",
    allowed_tools: ["search"],
    tool_context: %{tenant_id: "acme"}
  )
```

## Request Event Streaming

Use `ask_stream/3` when a caller needs canonical ReAct runtime events while a
request is running:

```elixir
{:ok, %{request: request, events: events}} =
  MyApp.MathAgent.ask_stream(pid, "Show your work")

for event <- events do
  IO.inspect({event.kind, event.data})
end

{:ok, result} = MyApp.MathAgent.await(request)
```

The enumerable yields `%Jido.AI.Runtime.Event{}` values and stops after
`:request_completed`, `:request_failed`, or `:request_cancelled`.

ReAct worker events arrive in runtime order. For a new run, the runtime assigns
sequence numbers from `1`. The final runtime checkpoint arrives before the
terminal event, while the stream sink is still active. You can read its token
from `event.data.token` when `event.kind == :checkpoint` and
`event.data.reason == :terminal`.

If a provider raises during a run, the failure events continue the sequence.
The failure checkpoint retains completed turns and partial streamed text.

Synthetic rejection and caller cancellation events use `seq: 0`. These events
can close the request stream before its worker finishes. An explicit
`stream_event_timeout_ms` can also stop the enumerable before a terminal event.

For mailbox-oriented integrations, pass a pid sink directly:

```elixir
{:ok, request} =
  MyApp.MathAgent.ask(pid, "Show your work",
    stream_to: {:pid, self()}
  )

receive do
  {:jido_ai_request_event, %Jido.AI.Runtime.Event{} = event} ->
    IO.inspect(event.kind)
end
```

Pid sinks are request-scoped and use the calling process mailbox. They do not
provide backpressure; keep handlers lightweight and always consume terminal
events so request streams close cleanly.

### Stream Sinks And Durable Checkpoints

A stream sink is a live pid. It is kept on the request only while the request is
active:

- Reaching a terminal state (completed, failed, or cancelled) drops the sink.
- Checkpoints never contain it, including while a request is still active.

Cancelling a streamed request emits `:request_cancelled` to the sink before the
sink is dropped.

### Restoring An Interrupted Streamed Request

A stream cannot continue after restore. A request that was active when the
checkpoint was written is restored as:

```elixir
%{
  status: :failed,
  error: :stream_interrupted,
  stream_interrupted: true
}
```

The restored request has no `:stream_to`. `await/2` returns
`{:error, :stream_interrupted}` for its old handle. Send a new request to start
a new stream.

## Steering An Active ReAct Run

`ask/await` remains the request API. Mid-run steering is a separate control path:

```elixir
{:ok, request} = MyApp.MathAgent.ask(pid, "Work on Q1")

{:ok, _agent} = MyApp.MathAgent.steer(pid, "Actually prioritize Q2", expected_request_id: request.id)

{:ok, result} = MyApp.MathAgent.await(request)
```

Use:
- `steer/3` for user-visible follow-up input on an active ReAct run
- `inject/3` for programmatic or inter-agent input on an active ReAct run

Important:
- neither `steer/3` nor `inject/3` creates a new request handle
- both reject idle agents with `{:error, {:rejected, :idle}}`
- successful `steer/3` / `inject/3` means the input was queued, not durably persisted
- if the run terminates before the runtime drains queued input, that input is dropped
- normal concurrent `ask/3` calls still busy-reject while a ReAct run is active
- steering is ReAct-only in this version

## Runtime Contract Map

- `Jido.AI.Request`: request handles, `await/2`, `await_many/2`, request state lifecycle.
- `Jido.AI.Turn`: normalized response shape and assistant/tool message projection.
- `Jido.AI.Context`: conversation accumulation and context projection for follow-up turns.
- `Jido.AI.steer/3` and `Jido.AI.inject/3`: explicit control path for active ReAct runs.
- Directive runtime behavior is documented in [Directives Runtime Contract](../developer/directives_runtime_contract.md).

## Await Many

```elixir
handles =
  ["2 + 2", "5 + 5", "8 + 8"]
  |> Enum.map(fn q -> elem(MyApp.MathAgent.ask(pid, q), 1) end)

results = Jido.AI.Request.await_many(handles, timeout: 30_000)
# [{:ok, ...}, {:ok, ...}, {:error, ...}]
```

## Runtime End-To-End Snippet

```elixir
alias Jido.AI.{Context, Turn}

{:ok, request} = MyApp.MathAgent.ask(pid, "What is 2 + 2?")

context =
  Context.new(system_prompt: "You are concise.")
  |> Context.append_user("What is 2 + 2?")

case MyApp.MathAgent.await(request, timeout: 15_000) do
  {:ok, result_text} ->
    turn = Turn.from_result_map(%{type: :final_answer, text: result_text})

    updated_context =
      context
      |> Context.append_assistant(turn.text)

    Context.to_messages(updated_context)

  {:error, {:rejected, :busy, message}} ->
    IO.puts("Request rejected: #{message}")

  {:error, :timeout} ->
    IO.puts("Request timed out")
end
```

## Lifecycle States

Each request is tracked with status like:
- `:pending`
- `:completed`
- `:failed`
- `:timeout`

Agent state keeps request maps and compatibility fields (`last_query`, `last_answer`, etc.).

Completed request records may also include normalized `meta` when the runtime
has it available. Common keys are:
- `:usage`
- `:reasoning_details`
- `:thinking_trace`
- `:last_thinking`

Example:

```elixir
{:ok, status} = Jido.AgentServer.status(pid)
request_id = status.raw_state[:last_request_id]

get_in(status.raw_state, [:requests, request_id, :meta])
# %{usage: %{...}, reasoning_details: [...], ...}
```

The status snapshot separates the final assistant answer from completed tool
outputs:

```elixir
{:ok, status} = Jido.AgentServer.status(pid)

answer = status.snapshot.result
tool_results = status.snapshot.details[:tool_results] || []
```

`tool_results` is scoped to the current or most recent ReAct run and is meant
for inspection. Persist durable business data from the tool itself, or return an
allowed state effect when later turns need to read it.

## Failure Mode: Timeouts Under Load

Symptom:
- frequent `{:error, :timeout}` from `await/2`

Fix:
- increase await timeout for expensive workloads
- lower `max_iterations` for ReAct-style loops
- reduce concurrency burst size or shard traffic

## Defaults You Should Know

- Default await timeout: `30_000ms`
- Default tracked request retention: `100` (evicts older entries)

## When To Use / Not Use

Use this pattern when:
- multiple caller processes can query the same agent
- you need precise correlation from submission to result

Do not use this pattern when:
- you only run single sequential calls and can tolerate sync wrappers

## Next

- [Context And Message Projection](thread_context_and_message_projection.md)
- [Observability Basics](observability_basics.md)
- [Architecture And Runtime Flow](../developer/architecture_and_runtime_flow.md)
