Nous.Transcript (nous v0.13.3)

View Source

Lightweight conversation history compaction.

Provides utility functions for managing conversation message lists without requiring an LLM call. For LLM-powered summarization, see Nous.Plugins.Summarization.

Usage

messages = [msg1, msg2, msg3, ..., msg20]

# Keep last 10 messages, summarize the rest
compacted = Nous.Transcript.compact(messages, 10)

# Auto-compact: every 20 messages, keep last 10
compacted = Nous.Transcript.maybe_compact(messages, every: 20, keep_last: 10)

# Auto-compact: at 80% of token budget
compacted = Nous.Transcript.maybe_compact(messages,
  token_budget: 128_000,
  keep_last: 10
)

# Both triggers (whichever fires first)
compacted = Nous.Transcript.maybe_compact(messages,
  every: 30,
  token_budget: 128_000,
  threshold: 0.8,
  keep_last: 10
)

# Run compaction in the background (returns a Task)
task = Nous.Transcript.compact_async(messages, 10)
compacted = Task.await(task)

# Fire-and-forget with callback
Nous.Transcript.compact_async(messages, 10, fn compacted ->
  send(self(), {:compacted, compacted})
end)

# Estimate token count
tokens = Nous.Transcript.estimate_tokens("Hello world, how are you?")
#=> 5

Summary

Functions

Compacts a message list by keeping the last keep_last messages.

Compacts messages asynchronously under Nous.TaskSupervisor.

Compacts messages in the background with a callback.

Estimates total tokens across a list of messages.

Estimates the token count of a string using word count as a proxy.

Automatically compacts messages when a trigger condition is met.

Like maybe_compact/2 but runs asynchronously with a callback.

Checks if a message list should be compacted based on a threshold.

Functions

compact(messages, keep_last)

@spec compact([Nous.Message.t()], pos_integer()) :: [Nous.Message.t()]

Compacts a message list by keeping the last keep_last messages.

If messages exceed the threshold, older messages are replaced with a summary system message. System messages at the start are always preserved.

Returns the original list if it's already within the limit.

Examples

iex> messages = for i <- 1..20, do: Nous.Message.user("Message #{i}")
iex> compacted = Nous.Transcript.compact(messages, 10)
iex> length(compacted)
11

compact_async(messages, keep_last)

@spec compact_async([Nous.Message.t()], pos_integer()) :: Task.t()

Compacts messages asynchronously under Nous.TaskSupervisor.

Returns a Task that resolves to the compacted message list. Useful when compaction runs inside a GenServer and you don't want to block the current process.

Examples

task = Nous.Transcript.compact_async(messages, 10)
# ... do other work ...
compacted = Task.await(task)

With a callback (fire-and-forget)

Nous.Transcript.compact_async(messages, 10, fn compacted ->
  send(self(), {:compacted, compacted})
end)

compact_async(messages, keep_last, callback)

@spec compact_async([Nous.Message.t()], pos_integer(), ([Nous.Message.t()] -> any())) ::
  {:ok, pid()}

Compacts messages in the background with a callback.

Starts a fire-and-forget task under Nous.TaskSupervisor. The callback receives the compacted message list when done. Returns {:ok, pid}.

Examples

{:ok, _pid} = Nous.Transcript.compact_async(messages, 10, fn compacted ->
  GenServer.cast(self, {:update_messages, compacted})
end)

estimate_messages_tokens(messages)

@spec estimate_messages_tokens([Nous.Message.t()]) :: non_neg_integer()

Estimates total tokens across a list of messages.

Examples

iex> messages = [Nous.Message.user("Hello"), Nous.Message.assistant("Hi there")]
iex> Nous.Transcript.estimate_messages_tokens(messages)
3

estimate_tokens(text)

@spec estimate_tokens(String.t() | nil) :: non_neg_integer()

Estimates the token count of a string using word count as a proxy.

This is a rough estimate (~1.3 tokens per word for English text). For precise counting, use a proper tokenizer.

Examples

iex> Nous.Transcript.estimate_tokens("Hello world")
2

iex> Nous.Transcript.estimate_tokens("")
0

maybe_compact(messages, opts)

@spec maybe_compact(
  [Nous.Message.t()],
  keyword()
) :: [Nous.Message.t()]

Automatically compacts messages when a trigger condition is met.

Returns the original messages unchanged if no trigger fires. Supports message count, token budget, or both (OR logic).

Options

  • :every — compact when message count exceeds this number
  • :token_budget — total token budget for the conversation
  • :threshold — fraction of token budget that triggers compaction (default 0.8)
  • :keep_last — how many recent messages to keep (required)

Examples

# Compact every 20 messages
messages = Nous.Transcript.maybe_compact(messages, every: 20, keep_last: 10)

# Compact at 80% of 128k token budget
messages = Nous.Transcript.maybe_compact(messages,
  token_budget: 128_000,
  keep_last: 10
)

# Both triggers — whichever fires first
messages = Nous.Transcript.maybe_compact(messages,
  every: 30,
  token_budget: 128_000,
  threshold: 0.75,
  keep_last: 10
)

maybe_compact_async(messages, opts, callback)

@spec maybe_compact_async([Nous.Message.t()], keyword(), (term() -> any())) ::
  {:ok, pid()}

Like maybe_compact/2 but runs asynchronously with a callback.

The callback receives {:compacted, messages} if compaction happened, or {:unchanged, messages} if no trigger fired.

Examples

Nous.Transcript.maybe_compact_async(messages,
  [every: 20, keep_last: 10],
  fn
    {:compacted, msgs} -> GenServer.cast(self, {:update, msgs})
    {:unchanged, _msgs} -> :ok
  end
)

should_compact?(messages, compact_after)

@spec should_compact?([Nous.Message.t()], pos_integer()) :: boolean()

Checks if a message list should be compacted based on a threshold.

Examples

iex> messages = for i <- 1..25, do: Nous.Message.user("msg #{i}")
iex> Nous.Transcript.should_compact?(messages, 20)
true