Packages

Prompt Runner SDK - packet-first prompt execution for Elixir and CLI workflows with verifier-owned completion, retry, repair, and git-aware repository orchestration.

Current section

Files

Jump to
prompt_runner_sdk lib prompt_runner session.ex
Raw

lib/prompt_runner/session.ex

defmodule PromptRunner.Session do
@moduledoc """
Bridge between Prompt Runner and AgentSessionManager adapters.
Starts the appropriate adapter (Claude, Codex, or Amp), runs a single prompt,
streams canonical ASM events, and provides a cleanup function for resource
teardown.
Delegates stream lifecycle management to `AgentSessionManager.StreamSession`.
"""
alias AgentSessionManager.Adapters.{
AmpAdapter,
ClaudeAdapter,
CodexAdapter
}
alias AgentSessionManager.StreamSession
alias PromptRunner.LLMFacade
@agent_id "prompt-runner"
@type provider :: PromptRunner.LLM.provider()
@type llm_config :: map()
@type stream_event :: map()
@type stream :: Enumerable.t()
@type close_fun :: (-> :ok)
@doc """
Starts a streaming prompt session and returns a lazy event stream.
"""
@spec start_stream(llm_config(), String.t()) ::
{:ok, stream(), close_fun(), map()} | {:error, term()}
def start_stream(llm_config, prompt) when is_map(llm_config) and is_binary(prompt) do
with {:ok, provider} <- normalize_provider(llm_config),
{:ok, adapter_spec} <- build_adapter_spec(provider, llm_config) do
run_opts = build_run_opts(llm_config)
case StreamSession.start(
adapter: adapter_spec,
input: %{messages: [%{role: "user", content: prompt}]},
agent_id: @agent_id,
run_opts: run_opts
) do
{:ok, stream, close_fun, _stream_meta} ->
meta = %{
sdk: provider,
model: llm_config[:model],
cwd: llm_config[:cwd]
}
{:ok, stream, close_fun, meta}
{:error, reason} ->
{:error, reason}
end
end
end
# -- Adapter spec builders
defp build_adapter_spec(:claude, llm_config) do
opts =
[]
|> maybe_put(:model, resolve_claude_model(llm_config[:model]))
|> maybe_put(:cwd, llm_config[:cwd])
|> maybe_put(:tools, llm_config[:allowed_tools])
|> maybe_put(:permission_mode, llm_config[:permission_mode])
|> maybe_put(:max_turns, llm_config[:max_turns])
|> maybe_put(:system_prompt, llm_config[:system_prompt])
|> maybe_put(:sdk_opts, llm_config[:sdk_opts])
|> Keyword.merge(normalize_opts(llm_config[:claude_opts]))
|> Keyword.merge(normalize_opts(llm_config[:adapter_opts]))
{:ok, {ClaudeAdapter, opts}}
end
defp build_adapter_spec(:codex, llm_config) do
with {:ok, cwd} <- require_cwd(llm_config, :codex) do
codex_sdk_opts =
llm_config[:sdk_opts]
|> normalize_opts()
|> Keyword.merge(normalize_opts(llm_config[:codex_opts]))
|> Keyword.merge(normalize_opts(llm_config[:codex_thread_opts]))
opts =
[]
|> maybe_put(:model, llm_config[:model])
|> maybe_put(:working_directory, cwd)
|> maybe_put(:permission_mode, llm_config[:permission_mode])
|> maybe_put(:max_turns, llm_config[:max_turns])
|> maybe_put(:system_prompt, llm_config[:system_prompt])
|> maybe_put(:sdk_opts, codex_sdk_opts)
|> Keyword.merge(normalize_opts(llm_config[:adapter_opts]))
|> ensure_option(:working_directory, cwd)
{:ok, {CodexAdapter, opts}}
end
end
defp build_adapter_spec(:amp, llm_config) do
with {:ok, cwd} <- require_cwd(llm_config, :amp) do
opts =
[]
|> maybe_put(:cwd, cwd)
|> maybe_put(:permission_mode, llm_config[:permission_mode])
|> maybe_put(:max_turns, llm_config[:max_turns])
|> maybe_put(:system_prompt, llm_config[:system_prompt])
|> maybe_put(:sdk_opts, llm_config[:sdk_opts])
|> Keyword.merge(normalize_opts(llm_config[:adapter_opts]))
|> ensure_option(:cwd, cwd)
{:ok, {AmpAdapter, opts}}
end
end
# -- Run opts
defp build_run_opts(llm_config) do
[]
|> maybe_put(:context, llm_config[:context])
|> maybe_put(:continuation, llm_config[:continuation])
|> maybe_put(:continuation_opts, llm_config[:continuation_opts])
end
# -- Helpers
defp require_cwd(llm_config, provider) do
cwd = llm_config[:cwd]
if is_binary(cwd) and cwd != "" do
{:ok, cwd}
else
{:error, {:missing_cwd, provider}}
end
end
defp normalize_provider(llm_config) do
candidate = llm_config[:provider] || llm_config[:sdk]
case LLMFacade.normalize_provider(candidate) do
provider when provider in [:claude, :codex, :amp] -> {:ok, provider}
{:error, reason} -> {:error, reason}
end
end
defp resolve_claude_model(nil), do: nil
defp resolve_claude_model("haiku"), do: "claude-haiku-4-5-20251001"
defp resolve_claude_model("sonnet"), do: "claude-sonnet-4-5-20250929"
defp resolve_claude_model("opus"), do: "claude-opus-4-6"
defp resolve_claude_model(model), do: model
defp normalize_opts(nil), do: []
defp normalize_opts(opts) when is_list(opts), do: opts
defp normalize_opts(opts) when is_map(opts), do: Enum.into(opts, [])
defp normalize_opts(_opts), do: []
defp ensure_option(opts, key, value) do
if Keyword.get(opts, key) in [nil, ""] do
Keyword.put(opts, key, value)
else
opts
end
end
defp maybe_put(opts, _key, nil), do: opts
defp maybe_put(opts, key, value), do: Keyword.put(opts, key, value)
end