MingaAgent.Session (Minga v0.1.0)

Copy Markdown View Source

Manages the lifecycle of one AI agent conversation.

The session holds conversation history, tracks agent status, and coordinates between the provider 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.

Summary

Types

Active tool call tracked while the provider is executing tools.

Remote attachment role.

Snapshot of session state needed by the editor for rendering.

File touch record.

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

Pending tool approval data.

Internal session state.

Agent session status.

Context inherited by child subagent sessions.

Tool trust lifetime.

Functions

Aborts the current agent operation.

Activates a skill by name.

Appends a system message to the conversation and notifies subscribers.

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

Branches the conversation at the given turn index.

Claims the driver role for a subscribed client when the role is vacant.

Clears all edit boundaries for this session.

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

Clears both queues without returning their contents.

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

Manually triggers context compaction on the provider.

Continues from an interrupted stream response.

Cycles to the next model in the configured rotation.

Cycles to the next thinking level.

Deactivates a skill by name.

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

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

Leaves plan mode and returns the session to execution mode.

Enters plan mode, where destructive tools are refused before execution.

Fetches available models from the provider.

Fetches available commands from the provider.

Returns the provider pid for direct provider-specific calls.

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

Returns whether hooks are enabled for this session.

Lists all conversation branches.

Lists all discovered skills and which are active.

Lists trusted tools and their trust scope.

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

Returns the conversation messages.

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

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

Starts a fresh conversation.

Returns whether this session persists its conversation to disk.

Returns the set of pinned message IDs.

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

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

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.

Re-checks whether any provider credential is now configured.

Responds to a pending tool approval.

Responds to a pending tool approval as an attached remote client.

Responds to a pending tool approval by stable approval id as an attached driver.

Revokes trust for one tool, or all tools with :all.

Seeds a session transcript without sending a prompt.

Sends a user prompt to the agent.

Sends a user prompt as an attached remote client.

Returns the session ID.

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

Sets the model without resetting conversation context.

Sets the thinking level on the provider.

Trusts a tool for the session or current turn.

Starts a new agent session.

Returns the current session status.

Returns the provider context that should be inherited by a subagent.

Subscribes the calling process to session events.

Subscribes the given process to session events.

Returns the current remote attachment role for a subscriber.

Generates a context artifact summarizing the current session.

Switches to a named branch, replacing the current messages.

Toggles all tool call messages between collapsed and expanded.

Toggles the pinned state of a message by its stable ID.

Toggles the collapsed state of a tool call message.

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

Unsubscribes the calling process from session events.

Unsubscribes the given process from session events.

Returns accumulated token usage.

Types

active_tool_call()

@type active_tool_call() :: {tool_call_id :: String.t(), name :: String.t()}

Active tool call tracked while the provider is executing tools.

approval_decision()

@type approval_decision() :: :approve | :approve_session | :approve_turn | :reject

attachment_role()

@type attachment_role() :: :driver | :viewer

Remote attachment role.

editor_snapshot()

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

Snapshot of session state needed by the editor for rendering.

error_kind()

@type error_kind() ::
  :rejected_key
  | :rate_limited
  | :auth_failed
  | :unreachable
  | :raw_dump
  | :passthrough

file_touch()

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

File touch record.

metadata()

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

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

pending_approval()

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

Pending tool approval data.

state()

@type state() :: %{
  session_id: String.t(),
  remote_token: String.t() | nil,
  workdir: String.t() | nil,
  event_log_server: GenServer.server(),
  provider: pid() | nil,
  provider_module: module(),
  provider_id: String.t(),
  provider_source: Minga.Extension.ContributionCleanup.contribution_source(),
  provider_lease: Minga.Extension.CodeLease.t() | nil,
  provider_opts: keyword(),
  status: status(),
  messages: [MingaAgent.Message.t()],
  message_ids: [pos_integer()],
  next_message_id: pos_integer(),
  subscribers: MapSet.t(pid()),
  subscriber_roles: %{required(pid()) => attachment_role()},
  driver: pid() | nil,
  idle_gc_timeout_ms: non_neg_integer(),
  idle_gc_timer: {timer_ref :: reference(), token :: reference()} | nil,
  idle_gc_token_fn: (-> reference()),
  total_usage: MingaAgent.Event.token_usage(),
  error_message: String.t() | nil,
  pending_thinking_level: String.t() | nil,
  pending_approval: pending_approval() | nil,
  active_tool_calls: [active_tool_call()],
  active_tool_name: String.t() | nil,
  turn_active?: boolean(),
  trust_levels: %{required(String.t()) => trust_scope()},
  pending_auto_approvals: %{required(String.t()) => trust_scope()},
  model_name: String.t(),
  provider_name: String.t(),
  notifier: module() | {module(), term()},
  background_subagent: boolean(),
  persist?: boolean(),
  hooks_enabled?: boolean(),
  session_start_hook_enabled?: boolean(),
  save_timer: reference() | nil,
  save_retry_count: non_neg_integer(),
  session_store_dir: String.t() | nil,
  created_at: DateTime.t(),
  last_message_at: DateTime.t(),
  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()) => file_touch()},
  boundaries: %{required(String.t()) => MingaAgent.EditBoundary.t()},
  pinned_ids: MapSet.t(pos_integer()),
  credentials_configured: boolean()
}

Internal session state.

status()

@type status() :: :idle | :plan | :thinking | :tool_executing | :error

Agent session status.

subagent_context()

@type subagent_context() :: MingaAgent.SubagentContext.t()

Context inherited by child subagent sessions.

trust_scope()

@type trust_scope() :: :session | :turn

Tool trust lifetime.

Functions

abort(session)

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

Aborts the current agent operation.

activate_skill(session, name)

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

Activates a skill by name.

add_system_message(session, text, level \\ :info)

@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(session, path)

@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(session, turn_index)

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

Branches the conversation at the given turn index.

claim_driver(session, pid)

@spec claim_driver(GenServer.server(), pid()) ::
  :ok | {:error, :driver_taken | :not_subscribed}

Claims the driver role for a subscribed client when the role is vacant.

clear_all_boundaries(session)

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

Clears all edit boundaries for this session.

clear_boundary(session, path)

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

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

clear_queues(session)

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

Clears both queues without returning their contents.

combine_queue_entries_to_text(entries)

@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(session)

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

Manually triggers context compaction on the provider.

continue(session)

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

Continues from an interrupted stream response.

cycle_model(session)

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

Cycles to the next model in the configured rotation.

cycle_thinking_level(session)

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

Cycles to the next thinking level.

deactivate_skill(session, name)

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

Deactivates a skill by name.

dequeue_steering(session)

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

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

editor_snapshot(session)

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

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

enter_exec(session)

@spec enter_exec(GenServer.server()) :: :ok

Leaves plan mode and returns the session to execution mode.

enter_plan(session)

@spec enter_plan(GenServer.server()) :: :ok

Enters plan mode, where destructive tools are refused before execution.

get_available_models(session)

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

Fetches available models from the provider.

get_commands(session)

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

Fetches available commands from the provider.

get_provider(session)

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

Returns the provider pid for direct provider-specific calls.

get_queued_messages(session)

@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).

hooks_enabled?(session)

@spec hooks_enabled?(GenServer.server()) :: boolean()

Returns whether hooks are enabled for this session.

list_branches(session)

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

Lists all conversation branches.

list_skills(session)

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

Lists all discovered skills and which are active.

list_tool_trust(session)

@spec list_tool_trust(GenServer.server()) :: %{required(String.t()) => trust_scope()}

Lists trusted tools and their trust scope.

load_session(session, session_id)

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

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

The current session is saved before replacement. The restored conversation history, branches, model, and metadata become the active session state.

messages(session)

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

Returns the conversation messages.

messages_with_ids(session)

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

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

metadata(session)

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

new_session(session)

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

Starts a fresh conversation.

persist?(session)

@spec persist?(GenServer.server()) :: boolean()

Returns whether this session persists its conversation to disk.

pinned_ids(session)

@spec pinned_ids(GenServer.server()) :: MapSet.t(pos_integer())

Returns the set of pinned message IDs.

queue_follow_up(session, content)

@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(session, content)

@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(session)

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.

refresh_credentials(session)

@spec refresh_credentials(GenServer.server()) :: :ok

Re-checks whether any provider credential is now configured.

Call after /auth or /login so the current session stops gating prompts and the UI's "not configured" state clears without a restart.

respond_to_approval(session, decision)

@spec respond_to_approval(GenServer.server(), approval_decision()) ::
  :ok | {:error, :no_pending_approval}

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.

respond_to_approval_as(session, client_pid, decision)

@spec respond_to_approval_as(GenServer.server(), pid(), approval_decision()) ::
  :ok | {:error, :no_pending_approval | :not_driver}

Responds to a pending tool approval as an attached remote client.

respond_to_approval_as(session, client_pid, approval_id, decision)

@spec respond_to_approval_as(
  GenServer.server(),
  pid(),
  String.t() | nil,
  approval_decision()
) ::
  :ok | {:error, :approval_not_found | :no_pending_approval | :not_driver}

Responds to a pending tool approval by stable approval id as an attached driver.

revoke_tool_trust(session, name_or_all)

@spec revoke_tool_trust(GenServer.server(), String.t() | :all) :: :ok

Revokes trust for one tool, or all tools with :all.

seed_messages(session, messages)

@spec seed_messages(GenServer.server(), [MingaAgent.Message.t()]) :: :ok

Seeds a session transcript without sending a prompt.

send_prompt(session, content)

@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).

send_prompt_as(session, client_pid, content)

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

Sends a user prompt as an attached remote client.

session_id(session)

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

Returns the session ID.

set_boundary(session, path, start_line, end_line)

@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(session, model)

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

Sets the model without resetting conversation context.

set_thinking_level(session, level)

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

Sets the thinking level on the provider.

set_tool_trust(session, name, scope)

@spec set_tool_trust(GenServer.server(), String.t(), trust_scope()) :: :ok

Trusts a tool for the session or current turn.

start_link(opts)

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

Starts a new agent session.

status(session)

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

Returns the current session status.

subagent_context(session)

@spec subagent_context(GenServer.server()) :: subagent_context()

Returns the provider context that should be inherited by a subagent.

subscribe(session)

@spec subscribe(GenServer.server()) :: :ok | {:error, :invalid_role}

Subscribes the calling process to session events.

subscribe(session, pid, opts \\ [])

@spec subscribe(GenServer.server(), pid(), keyword()) :: :ok | {:error, :invalid_role}

Subscribes the given process to session events.

subscriber_role(session, pid)

@spec subscriber_role(GenServer.server(), pid()) :: attachment_role() | nil

Returns the current remote attachment role for a subscriber.

summarize(session)

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

Generates a context artifact summarizing the current session.

switch_branch(session, branch_index)

@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(session)

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

Toggles all tool call messages between collapsed and expanded.

toggle_pin(session, message_id)

@spec toggle_pin(GenServer.server(), pos_integer()) :: :ok

Toggles the pinned state of a message by its stable ID.

toggle_tool_collapse(session, message_index)

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

Toggles the collapsed state of a tool call message.

touched_files(session)

@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, apply_diff).

unsubscribe(session)

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

Unsubscribes the calling process from session events.

unsubscribe(session, pid)

@spec unsubscribe(GenServer.server(), pid()) :: :ok

Unsubscribes the given process from session events.

usage(session)

Returns accumulated token usage.