# `AgentSessionManager.Ports.QueryAPI`
[🔗](https://github.com/nshkrdotcom/agent_session_manager/blob/v0.8.0/lib/agent_session_manager/ports/query_api.ex#L1)

Read-only query interface for historical session data.

Provides cross-session search, aggregation, and export capabilities
beyond the single-session cursor reads in SessionStore.

## Callbacks

- `search_sessions/2` — Search sessions with filters and cursor pagination
- `get_session_stats/2` — Aggregate statistics for a session
- `search_runs/2` — Search runs across sessions
- `get_usage_summary/2` — Token usage summary by provider
- `get_cost_summary/2` — Cost summary by provider/model
- `search_events/2` — Search events across sessions
- `count_events/2` — Count events matching filters
- `export_session/3` — Export complete session data

## Usage

    {:ok, %{sessions: sessions, cursor: cursor}} =
      QueryAPI.search_sessions({EctoQueryAPI, MyApp.Repo}, agent_id: "agent-1", limit: 20)

    {:ok, stats} = QueryAPI.get_session_stats({EctoQueryAPI, MyApp.Repo}, "ses_abc123")

# `context`

```elixir
@type context() :: term()
```

# `cursor`

```elixir
@type cursor() :: String.t() | nil
```

# `query_ref`

```elixir
@type query_ref() :: {module(), context()}
```

# `count_events`

```elixir
@callback count_events(
  context(),
  keyword()
) :: {:ok, non_neg_integer()} | {:error, AgentSessionManager.Core.Error.t()}
```

Count events matching filters without loading them.

Same filter options as `search_events/2`.

# `export_session`

```elixir
@callback export_session(context(), session_id :: String.t(), keyword()) ::
  {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

Export a session's complete data (session + runs + events) as a map.

## Options

- `:include_artifacts` — include artifact metadata (default: false)

# `get_cost_summary`

```elixir
@callback get_cost_summary(
  context(),
  keyword()
) :: {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

Get cost summary across runs.

## Options

- `:session_id` — scope to session
- `:agent_id` — scope to agent
- `:provider` — filter by provider
- `:since` — DateTime lower bound
- `:until` — DateTime upper bound
- `:pricing_table` — override default pricing (optional)

Returns a map with:
- `total_cost_usd` — float
- `run_count` — integer
- `by_provider` — per-provider cost breakdown
- `by_model` — per-model cost breakdown
- `unmapped_runs` — count of runs without cost

# `get_session_stats`

```elixir
@callback get_session_stats(context(), session_id :: String.t()) ::
  {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

Get aggregate statistics for a session.

Returns a map with:
- `event_count` — total events
- `run_count` — total runs
- `token_totals` — `%{input_tokens: n, output_tokens: n, total_tokens: n}`
- `providers_used` — list of provider names
- `first_event_at` — earliest event timestamp
- `last_event_at` — latest event timestamp
- `status_counts` — `%{completed: 3, failed: 1, ...}` (run statuses)

# `get_usage_summary`

```elixir
@callback get_usage_summary(
  context(),
  keyword()
) :: {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

Get token usage summary across runs.

## Options

- `:session_id` — scope to session
- `:provider` — filter by provider
- `:since` — DateTime lower bound
- `:until` — DateTime upper bound

Returns a map with:
- `total_input_tokens` — integer
- `total_output_tokens` — integer
- `total_tokens` — integer
- `run_count` — integer
- `by_provider` — `%{"claude" => %{...}, "codex" => %{...}}`

# `search_events`

```elixir
@callback search_events(
  context(),
  keyword()
) ::
  {:ok, %{events: [AgentSessionManager.Core.Event.t()], cursor: cursor()}}
  | {:error, AgentSessionManager.Core.Error.t()}
```

Search events across sessions with rich filters.

## Options

- `:session_ids` — list of session IDs
- `:run_ids` — filter by specific runs
- `:types` — filter by event types (atom or list)
- `:providers` — filter by provider
- `:since` — events after this DateTime
- `:until` — events before this DateTime
- `:correlation_id` — filter by correlation ID
- `:order_by` — `:sequence_asc` (default) | `:timestamp_asc` | `:timestamp_desc`
- `:limit` — max results (default: 100)
- `:cursor` — opaque cursor from previous page

# `search_runs`

```elixir
@callback search_runs(
  context(),
  keyword()
) ::
  {:ok, %{runs: [AgentSessionManager.Core.Run.t()], cursor: cursor()}}
  | {:error, AgentSessionManager.Core.Error.t()}
```

Search runs across sessions.

## Options

- `:session_id` — scope to single session
- `:provider` — filter by provider
- `:status` — filter by status
- `:started_after` — runs started after this DateTime
- `:started_before` — runs started before this DateTime
- `:min_tokens` — minimum total_tokens
- `:order_by` — `:started_at_desc` (default) | `:started_at_asc` | `:token_usage_desc`
- `:limit` — max results (default: 50)
- `:cursor` — opaque cursor from previous page

# `search_sessions`

```elixir
@callback search_sessions(
  context(),
  keyword()
) ::
  {:ok,
   %{
     sessions: [AgentSessionManager.Core.Session.t()],
     cursor: cursor(),
     total_count: non_neg_integer()
   }}
  | {:error, AgentSessionManager.Core.Error.t()}
```

Search sessions with rich filters.

## Options

- `:agent_id` — filter by agent
- `:status` — filter by status (atom or list)
- `:provider` — filter by provider that executed runs in this session
- `:tags` — filter by tags (all must match)
- `:created_after` — sessions created after this DateTime
- `:created_before` — sessions created before this DateTime
- `:include_deleted` — include soft-deleted sessions (default: false)
- `:order_by` — `:created_at_asc` | `:created_at_desc` | `:updated_at_desc` (default)
- `:limit` — max results (default: 50)
- `:cursor` — opaque cursor from previous page

# `count_events`

```elixir
@spec count_events(
  query_ref(),
  keyword()
) :: {:ok, non_neg_integer()} | {:error, AgentSessionManager.Core.Error.t()}
```

# `export_session`

```elixir
@spec export_session(query_ref(), String.t(), keyword()) ::
  {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

# `get_cost_summary`

```elixir
@spec get_cost_summary(
  query_ref(),
  keyword()
) :: {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

# `get_session_stats`

```elixir
@spec get_session_stats(query_ref(), String.t()) ::
  {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

# `get_usage_summary`

```elixir
@spec get_usage_summary(
  query_ref(),
  keyword()
) :: {:ok, map()} | {:error, AgentSessionManager.Core.Error.t()}
```

# `search_events`

```elixir
@spec search_events(
  query_ref(),
  keyword()
) ::
  {:ok, %{events: [AgentSessionManager.Core.Event.t()], cursor: cursor()}}
  | {:error, AgentSessionManager.Core.Error.t()}
```

# `search_runs`

```elixir
@spec search_runs(
  query_ref(),
  keyword()
) ::
  {:ok, %{runs: [AgentSessionManager.Core.Run.t()], cursor: cursor()}}
  | {:error, AgentSessionManager.Core.Error.t()}
```

# `search_sessions`

```elixir
@spec search_sessions(
  query_ref(),
  keyword()
) ::
  {:ok,
   %{
     sessions: [AgentSessionManager.Core.Session.t()],
     cursor: cursor(),
     total_count: non_neg_integer()
   }}
  | {:error, AgentSessionManager.Core.Error.t()}
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
