# `MingaAgent.Session`
[🔗](https://github.com/jsmestad/minga/blob/main/lib/minga_agent/session.ex#L1)

Manages the lifecycle of one AI agent conversation.

The session holds conversation history, tracks agent status, and
coordinates between the provider (pi RPC, etc.) and the editor UI.
It runs as a supervised GenServer under `Agent.Supervisor`, so a
crash here never affects buffers or the editor.

## Status lifecycle

    :idle → :thinking → :tool_executing → :thinking → ... → :idle
               ↓                              ↓
            :error                          :error

## Subscribing to events

Call `subscribe/2` with a pid to receive `{:agent_event, session_pid, event}`
messages. The editor uses this to update the modeline and chat panel.

# `editor_snapshot`

```elixir
@type editor_snapshot() :: %{
  status: status(),
  pending_approval: map() | nil,
  error: String.t() | nil
}
```

Snapshot of session state needed by the editor for rendering.

# `file_touch`

```elixir
@type file_touch() :: %{
  path: String.t(),
  action: :created | :modified | :deleted,
  timestamp: integer()
}
```

File touch record.

# `metadata`

```elixir
@type metadata() :: MingaAgent.SessionMetadata.t()
```

Deprecated: use `MingaAgent.SessionMetadata.t()` directly.

# `pending_approval`

```elixir
@type pending_approval() :: MingaAgent.ToolApproval.t()
```

Pending tool approval data.

# `state`

```elixir
@type state() :: %{
  session_id: String.t(),
  provider: pid() | nil,
  provider_module: module(),
  provider_opts: keyword(),
  status: status(),
  messages: [MingaAgent.Message.t()],
  message_ids: [pos_integer()],
  next_message_id: pos_integer(),
  subscribers: MapSet.t(pid()),
  total_usage: MingaAgent.Event.token_usage(),
  error_message: String.t() | nil,
  pending_thinking_level: String.t() | nil,
  pending_approval: pending_approval() | nil,
  model_name: String.t(),
  provider_name: String.t(),
  save_timer: reference() | nil,
  branches: [MingaAgent.Branch.t()],
  steering_queue: [String.t() | [ReqLLM.Message.ContentPart.t()]],
  follow_up_queue: [String.t() | [ReqLLM.Message.ContentPart.t()]],
  touched_files: %{required(String.t()) =&gt; file_touch()},
  boundaries: %{required(String.t()) =&gt; MingaAgent.EditBoundary.t()}
}
```

Internal session state.

# `status`

```elixir
@type status() :: :idle | :thinking | :tool_executing | :error
```

Agent session status.

# `abort`

```elixir
@spec abort(GenServer.server()) :: :ok
```

Aborts the current agent operation.

# `activate_skill`

```elixir
@spec activate_skill(GenServer.server(), String.t()) ::
  {:ok, term()} | {:error, term()}
```

Activates a skill by name.

# `add_system_message`

```elixir
@spec add_system_message(
  GenServer.server(),
  String.t(),
  MingaAgent.Message.system_level()
) :: :ok
```

Appends a system message to the conversation and notifies subscribers.

# `boundary_for`

```elixir
@spec boundary_for(GenServer.server(), String.t()) ::
  {non_neg_integer(), non_neg_integer()} | nil
```

Returns the edit boundary for the given file path, or nil if unbounded.

# `branch_at`

```elixir
@spec branch_at(GenServer.server(), non_neg_integer()) ::
  {:ok, String.t()} | {:error, String.t()}
```

Branches the conversation at the given turn index.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear_all_boundaries`

```elixir
@spec clear_all_boundaries(GenServer.server()) :: :ok
```

Clears all edit boundaries for this session.

# `clear_boundary`

```elixir
@spec clear_boundary(GenServer.server(), String.t()) :: :ok
```

Clears the edit boundary for the given file path, restoring full-buffer access.

# `clear_queues`

```elixir
@spec clear_queues(GenServer.server()) :: :ok
```

Clears both queues without returning their contents.

# `combine_queue_entries_to_text`

```elixir
@spec combine_queue_entries_to_text([String.t() | [ReqLLM.Message.ContentPart.t()]]) ::
  String.t()
```

Converts a list of queue entries (strings or ContentPart lists) into a single
string suitable for display or restoring to the prompt input.

# `compact`

```elixir
@spec compact(GenServer.server()) :: {:ok, String.t()} | {:error, String.t()}
```

Manually triggers context compaction on the provider.

# `continue`

```elixir
@spec continue(GenServer.server()) :: :ok | {:error, term()}
```

Continues from an interrupted stream response.

# `cycle_model`

```elixir
@spec cycle_model(GenServer.server()) :: {:ok, map()} | {:error, term()}
```

Cycles to the next model in the configured rotation.

# `cycle_thinking_level`

```elixir
@spec cycle_thinking_level(GenServer.server()) :: {:ok, term()} | {:error, term()}
```

Cycles to the next thinking level.

# `deactivate_skill`

```elixir
@spec deactivate_skill(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Deactivates a skill by name.

# `dequeue_steering`

```elixir
@spec dequeue_steering(GenServer.server()) :: [
  String.t() | [ReqLLM.Message.ContentPart.t()]
]
```

Pops and returns all pending steering messages, clearing the steering queue.

# `editor_snapshot`

```elixir
@spec editor_snapshot(GenServer.server()) :: editor_snapshot()
```

Returns a snapshot of session state for the editor to rebuild AgentState.

# `get_available_models`

```elixir
@spec get_available_models(GenServer.server()) :: {:ok, term()} | {:error, term()}
```

Fetches available models from the provider.

# `get_commands`

```elixir
@spec get_commands(GenServer.server()) :: {:ok, [map()]} | {:error, term()}
```

Fetches available commands from the provider.

# `get_provider`

```elixir
@spec get_provider(GenServer.server()) :: pid() | nil
```

Returns the provider pid for direct provider-specific calls.

# `get_queued_messages`

```elixir
@spec get_queued_messages(GenServer.server()) ::
  {[String.t() | [ReqLLM.Message.ContentPart.t()]],
   [String.t() | [ReqLLM.Message.ContentPart.t()]]}
```

Returns both queues without modifying them (for pending message display).

# `list_branches`

```elixir
@spec list_branches(GenServer.server()) :: {:ok, String.t()}
```

Lists all conversation branches.

# `list_skills`

```elixir
@spec list_skills(GenServer.server()) ::
  {:ok, [map()], [String.t()]} | {:error, term()}
```

Lists all discovered skills and which are active.

# `load_session`

```elixir
@spec load_session(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Loads a previously saved session, replacing the current conversation history.

The provider's conversation context is not synced; the loaded messages
are for display only until the user sends a new prompt, which re-establishes
the provider context.

# `messages`

```elixir
@spec messages(GenServer.server()) :: [MingaAgent.Message.t()]
```

Returns the conversation messages.

# `messages_with_ids`

```elixir
@spec messages_with_ids(GenServer.server()) :: [
  {pos_integer(), MingaAgent.Message.t()}
]
```

Returns the conversation messages paired with their stable BEAM-assigned IDs.

# `metadata`

```elixir
@spec metadata(GenServer.server()) :: MingaAgent.SessionMetadata.t()
```

Returns lightweight metadata about this session (for the picker).

# `new_session`

```elixir
@spec new_session(GenServer.server()) :: :ok | {:error, term()}
```

Starts a fresh conversation.

# `queue_follow_up`

```elixir
@spec queue_follow_up(
  GenServer.server(),
  String.t() | [ReqLLM.Message.ContentPart.t()]
) ::
  :ok | {:queued, :follow_up} | {:error, term()}
```

Queues a message as a follow-up (sent automatically once the current agent run finishes).

When the agent is idle, behaves identically to `send_prompt/2`.
Returns `{:queued, :follow_up}` when the message was queued.

# `queue_steering`

```elixir
@spec queue_steering(
  GenServer.server(),
  String.t() | [ReqLLM.Message.ContentPart.t()]
) ::
  :ok | {:queued, :steering} | {:error, term()}
```

Queues a message as a steering prompt (injected between tool calls on the next turn).

When the agent is idle, behaves identically to `send_prompt/2`.
Returns `{:queued, :steering}` when the message was queued.

# `recall_queues`

```elixir
@spec recall_queues(GenServer.server()) ::
  {[String.t() | [ReqLLM.Message.ContentPart.t()]],
   [String.t() | [ReqLLM.Message.ContentPart.t()]]}
```

Returns both queues and clears them. Used by abort (Ctrl-C) and dequeue (Alt+Up)
so pending messages can be restored to the prompt input.

# `respond_to_approval`

```elixir
@spec respond_to_approval(GenServer.server(), :approve | :reject | :approve_all) ::
  :ok
```

Responds to a pending tool approval.

Sends the decision directly to the Task process that is blocking
on `receive`, then clears the pending approval and broadcasts
the resolution to subscribers.

# `send_prompt`

```elixir
@spec send_prompt(GenServer.server(), String.t() | [ReqLLM.Message.ContentPart.t()]) ::
  :ok | {:queued, :steering} | {:error, term()}
```

Sends a user prompt to the agent.

Accepts either a plain text string or a list of ContentPart structs
(for multi-modal messages with images).

# `session_id`

```elixir
@spec session_id(GenServer.server()) :: String.t()
```

Returns the session ID.

# `set_boundary`

```elixir
@spec set_boundary(
  GenServer.server(),
  String.t(),
  non_neg_integer(),
  non_neg_integer()
) ::
  :ok | {:error, String.t()}
```

Sets an edit boundary for the agent on the given file path.

The agent will be restricted to editing within the specified line range
(0-indexed, both inclusive). Edits outside the boundary are rejected with
a descriptive error message.

# `set_model`

```elixir
@spec set_model(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Sets the model without resetting conversation context.

# `set_thinking_level`

```elixir
@spec set_thinking_level(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Sets the thinking level on the provider.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Starts a new agent session.

# `status`

```elixir
@spec status(GenServer.server()) :: status()
```

Returns the current session status.

# `subscribe`

```elixir
@spec subscribe(GenServer.server()) :: :ok
```

Subscribes the calling process to session events.

# `summarize`

```elixir
@spec summarize(GenServer.server()) ::
  {:ok, String.t(), String.t()} | {:error, term()}
```

Generates a context artifact summarizing the current session.

# `switch_branch`

```elixir
@spec switch_branch(GenServer.server(), non_neg_integer()) ::
  :ok | {:error, String.t()}
```

Switches to a named branch, replacing the current messages.

# `toggle_all_tool_collapses`

```elixir
@spec toggle_all_tool_collapses(GenServer.server()) :: :ok
```

Toggles all tool call messages between collapsed and expanded.

# `toggle_tool_collapse`

```elixir
@spec toggle_tool_collapse(GenServer.server(), non_neg_integer()) :: :ok
```

Toggles the collapsed state of a tool call message.

# `touched_files`

```elixir
@spec touched_files(GenServer.server()) :: [file_touch()]
```

Returns files touched by this agent session, ordered by most recent first.

Each entry contains:
- `path`: relative file path
- `action`: `:created`, `:modified`, or `:deleted`
- `timestamp`: monotonic timestamp of the last touch

Derived from tool call history (file_write, file_edit, multi_edit_file).

# `unsubscribe`

```elixir
@spec unsubscribe(GenServer.server()) :: :ok
```

Unsubscribes the calling process from session events.

# `usage`

```elixir
@spec usage(GenServer.server()) :: MingaAgent.Event.token_usage()
```

Returns accumulated token usage.

---

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