PtcRunner.SubAgent.Compaction (PtcRunner v0.12.0)

Copy Markdown View Source

Pressure-triggered context compaction for multi-turn agents.

Compaction reduces the LLM-input message list when turn count or estimated token usage crosses a threshold. Recent turns are preserved verbatim; older turns are trimmed (Phase 1) or summarized (Phase 2 — not yet implemented).

Phase 1 ships one strategy: :trim. Custom strategy modules and :summarize are deferred to Phase 2.

Configuration

SubAgent.run(prompt, llm: llm, compaction: true)

SubAgent.run(prompt,
  llm: llm,
  compaction: [
    strategy: :trim,
    trigger: [turns: 8, tokens: 12_000],
    keep_recent_turns: 3,
    keep_initial_user: true,
    token_counter: nil
  ]
)

Defaults for compaction: true:

[
  strategy: :trim,
  trigger: [turns: 8],
  keep_recent_turns: 3,
  keep_initial_user: true,
  token_counter: nil
]

Library default is false — compaction is opt-in.

Token estimation

Default counter is String.length(content) / 4, matching the existing metrics heuristic. Override via token_counter: fun/1. This is explicitly a pressure heuristic and not model-accurate.

Summary

Functions

Build a Compaction.Context for a given turn.

Default token counter — String.length/1 divided by 4, with a floor of 1 for any non-empty content.

Default trim options used when compaction: true.

Run compaction for a list of LLM-input messages.

Normalize compaction configuration.

Types

message()

@type message() :: %{role: :user | :assistant, content: String.t()}

normalized()

@type normalized() :: {:disabled, []} | {:trim, keyword()}

stats()

@type stats() :: map()

Functions

build_context(loop_fields, opts)

Build a Compaction.Context for a given turn.

Resolves the token counter from opts (or falls back to the default).

default_token_counter(content)

@spec default_token_counter(String.t()) :: non_neg_integer()

Default token counter — String.length/1 divided by 4, with a floor of 1 for any non-empty content.

Mirrors PtcRunner.SubAgent.Loop.Metrics.estimate_tokens/1 so token-pressure detection still fires on histories made of short messages. Pressure heuristic, not adapter-accurate.

Examples

iex> PtcRunner.SubAgent.Compaction.default_token_counter("hello world")
2

iex> PtcRunner.SubAgent.Compaction.default_token_counter("hi")
1

iex> PtcRunner.SubAgent.Compaction.default_token_counter("")
0

default_trim_opts()

@spec default_trim_opts() :: keyword()

Default trim options used when compaction: true.

maybe_compact(messages, ctx, arg)

@spec maybe_compact(
  [message()],
  PtcRunner.SubAgent.Compaction.Context.t(),
  normalized()
) ::
  {[message()], stats() | nil}

Run compaction for a list of LLM-input messages.

normalized is the output of normalize/1. Always returns {messages, stats | nil}:

  • When the strategy is :disabled, returns {messages, nil} — no work, no stats.
  • Otherwise dispatches to the strategy and returns its {messages, stats} result. Use stats.triggered (boolean) to distinguish a triggered trim from a not-triggered pass-through; the stats shape is consistent either way.

normalize(opts)

@spec normalize(nil | boolean() | keyword()) :: normalized()

Normalize compaction configuration.

Accepts:

  • nil or false{:disabled, []}
  • true{:trim, default_opts}
  • keyword() with strategy: :trim (or unspecified) → {:trim, merged_opts}

Raises ArgumentError for invalid input. The error message is the source of truth for what Phase 1 supports.