ClaudeWrapper.Conversation (ClaudeWrapper v0.8.0)

Copy Markdown View Source

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:

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.

t()

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

session()

@type session() :: GenServer.server()

The underlying session reference: the pid or registered name of a running ClaudeWrapper.DuplexSession.

t()

@type t() :: %ClaudeWrapper.Conversation{
  history: [ClaudeWrapper.Result.t()],
  session: session()
}

Functions

close(conversation)

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

history(conversation)

@spec history(t()) :: [ClaudeWrapper.Result.t()]

The per-turn Result history, in arrival order.

last_result(conversation)

@spec last_result(t()) :: ClaudeWrapper.Result.t() | nil

The most recent turn's Result, or nil if no turn has completed.

new(session)

@spec new(session()) :: t()

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(conversation, prompt, timeout \\ 120_000)

@spec send(t(), String.t(), timeout()) ::
  {:ok, t(), ClaudeWrapper.Result.t()} | {:error, term()}

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.

session(conversation)

@spec session(t()) :: session()

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.

session_id(conversation)

@spec session_id(t()) :: String.t() | nil

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

total_cost(conversation)

@spec total_cost(t()) :: float()

Cumulative cost in USD across every recorded turn.

Turns whose Result carries no cost_usd contribute 0.0.

turn_count(conversation)

@spec turn_count(t()) :: non_neg_integer()

Number of turns recorded through send/2,3.