Host-side bookkeeping wrapper over a ClaudeWrapper.DuplexSession.
Conversation keeps a rolling history of ClaudeWrapper.Result
structs, cumulative cost, and a turn count on top of an underlying
long-lived DuplexSession. The duplex session remains the transport;
this wrapper only adds the accounting that DuplexSession.send/3 does
not provide on its own.
It is the duplex-flavoured peer of ClaudeWrapper.Session, which is
the equivalent bookkeeping shape over transient per-call subprocess
turns. Conversation follows the same functional-struct style: a
%Conversation{} value threads through send/2,3, which returns the
updated struct alongside the turn's Result.
Mirrors the Rust crate's conversation::Conversation.
When to use
Reach for Conversation when you already want a DuplexSession
(long-running host, mid-turn interrupts, broadcast subscribers) and
also want to answer:
- How much have I spent on this conversation so far? (
total_cost/1) - What is the full history of turns? (
history/1) - How many turns have completed? (
turn_count/1)
If you do not need bookkeeping, drive the DuplexSession directly. If
you want accounting over short-lived per-turn subprocess calls, use
ClaudeWrapper.Session instead.
Usage
config = ClaudeWrapper.Config.new()
{:ok, session} = ClaudeWrapper.DuplexSession.start_link(config: config)
conv = ClaudeWrapper.Conversation.new(session)
{:ok, conv, _result} = ClaudeWrapper.Conversation.send(conv, "hello")
{:ok, conv, _result} = ClaudeWrapper.Conversation.send(conv, "and again")
ClaudeWrapper.Conversation.turn_count(conv)
#=> 2
ClaudeWrapper.Conversation.total_cost(conv)
#=> 0.0123
ClaudeWrapper.Conversation.close(conv)Beyond bookkeeping
send/2,3 is the only entry point that records history. For
DuplexSession.subscribe/1, DuplexSession.interrupt/2, and
DuplexSession.respond_to_permission/3, reach the inner handle via
session/1. Those calls bypass the wrapper's accounting on purpose:
an interrupt still produces a Result that the in-flight send/2,3
records cleanly when the truncated turn lands.
Summary
Types
The underlying session reference: the pid or registered name of a
running ClaudeWrapper.DuplexSession.
Functions
Stop the underlying DuplexSession.
The per-turn Result history, in arrival order.
The most recent turn's Result, or nil if no turn has completed.
Wrap a running DuplexSession in a fresh conversation.
Send a user prompt over the underlying DuplexSession and record the
resulting ClaudeWrapper.Result.
Borrow the underlying DuplexSession reference.
The session id assigned by the CLI, or nil until the first turn (or
system/init) has been observed.
Cumulative cost in USD across every recorded turn.
Number of turns recorded through send/2,3.
Types
@type session() :: GenServer.server()
The underlying session reference: the pid or registered name of a
running ClaudeWrapper.DuplexSession.
@type t() :: %ClaudeWrapper.Conversation{ history: [ClaudeWrapper.Result.t()], session: session() }
Functions
@spec close(t()) :: :ok
Stop the underlying DuplexSession.
Delegates to DuplexSession.close/1 (graceful stop). Returns :ok.
The %Conversation{} struct is left as-is; its accumulated history
remains readable after the session is gone.
@spec history(t()) :: [ClaudeWrapper.Result.t()]
The per-turn Result history, in arrival order.
@spec last_result(t()) :: ClaudeWrapper.Result.t() | nil
The most recent turn's Result, or nil if no turn has completed.
Wrap a running DuplexSession in a fresh conversation.
The conversation starts with an empty history; the underlying session
is not touched until the first send/2,3. session is the pid (or
registered name) returned by DuplexSession.start_link/1.
Send a user prompt over the underlying DuplexSession and record the
resulting ClaudeWrapper.Result.
Returns {:ok, conversation, result} with the history-updated
conversation on success, matching ClaudeWrapper.Session.send/3's
return convention. Errors from DuplexSession.send/3 (e.g.
{:error, %ClaudeWrapper.Error{kind: :turn_in_flight}},
{:error, %ClaudeWrapper.Error{kind: :duplex_closed}}) propagate
unchanged and do not update the history.
The timeout defaults to the same 120 seconds as
DuplexSession.send/3, since the entire turn (cold start + model
latency + tool calls) must complete within it.
Borrow the underlying DuplexSession reference.
Use this for DuplexSession.subscribe/1, DuplexSession.interrupt/2,
and DuplexSession.respond_to_permission/3, which bypass this
wrapper's bookkeeping on purpose.
The session id assigned by the CLI, or nil until the first turn (or
system/init) has been observed.
Prefers the most recent turn's Result.session_id; falls back to
querying the underlying DuplexSession when no turn has recorded one
yet (for example, system/init arrived but no turn has completed).
Cumulative cost in USD across every recorded turn.
Turns whose Result carries no cost_usd contribute 0.0.
@spec turn_count(t()) :: non_neg_integer()
Number of turns recorded through send/2,3.