Skip to content

aimu.context

Plain functions over a conversation's list[dict]: estimate its size, trim it to a token budget, or replace the older part with an LLM-generated summary. No ContextPolicy class, no hidden rewriting inside a client. See how-to: manage context.

aimu.context.count_tokens

count_tokens(messages: list[dict], *, counter: Optional[Callable[[str], int]] = None) -> int

Estimate the token count of messages.

The default counter is len(text) // 4, a rule-of-thumb estimate -- it is not a real tokenizer and will be wrong (usually within 20-30%) for any specific model. Exact counts exist only after the fact, via client.last_usage on a real request. Pass counter= (any Callable[[str], int], e.g. a real tokenizer's encode+len) for accuracy.

text is each message JSON-serialized (structural overhead included, since it rides along in the real request) with AIMU's own inert bookkeeping keys (timestamp, thinking, provenance) stripped first, since a provider never sees those. See the module docstring for the full rationale.

aimu.context.trim_messages

trim_messages(messages: list[dict], max_tokens: int, *, keep_system: bool = True, keep_last: int = 2, counter: Optional[Callable[[str], int]] = None) -> list[dict]

Return a new, trimmed message list that fits within max_tokens (estimated).

Always returns a new list; messages (and its dicts) are never mutated. A conversation already at or under max_tokens is returned unchanged (a copy) -- this is a no-op, not a forced rewrite.

Dropping proceeds oldest-first over the non-system messages, one indivisible group at a time (see :func:_group_messages): an assistant message carrying tool_calls is always dropped together with every tool message answering it, never separately. This is the invariant that justifies this function over a hand-written messages[-n:] slice, which produces exactly that illegal shape.

Parameters:

Name Type Description Default
messages list[dict]

The conversation, OpenAI message-dict format.

required
max_tokens int

The token budget to trim down to, per counter (or the default estimate; see :func:count_tokens).

required
keep_system bool

When True (default), every {"role": "system"} message is kept regardless of budget, and is moved to the front of the returned list. That is invisible for the usual single leading system prompt, but a system message that sat mid-conversation (an earlier summary inserted by :func:summarize_messages, say) comes back ahead of the turns it originally followed. When False, system messages are ordinary droppable messages like any other and order is preserved.

True
keep_last int

The number of trailing messages (not exchanges -- see the module docstring) to always protect from dropping, extended outward to whole tool-call groups when the raw count would otherwise split one.

2
counter Optional[Callable[[str], int]]

Optional real tokenizer; see :func:count_tokens.

None

aimu.context.summarize_messages

summarize_messages(client: Any, messages: list[dict], *, keep_last: int = 2, prompt: Optional[str] = None) -> list[dict]

Replace the older part of a conversation with a one-message LLM summary.

Any {"role": "system"} messages are always preserved unchanged, and are returned at the front (a mid-conversation system message is hoisted, the same reordering :func:trim_messages does under keep_system=True). Of the rest, the last keep_last messages (see the module docstring on why messages, not exchanges; extended outward to whole tool-call groups per :func:_group_messages, the same invariant :func:trim_messages enforces) are kept verbatim as the tail. Everything older than that (the "prefix") is rendered to a plain transcript, summarized by one client.generate() call, and replaced with a single {"role": "system", ...} message carrying the summary text -- system, rather than user or assistant, because it is out-of-band context neither party actually said, not a real conversational turn.

client is any object exposing generate(prompt: str) -> str (a plain :class:~aimu.models.BaseModelClient, or a :class:~aimu.agents.Agent via agent.as_model_client()); this function takes it as an argument rather than constructing one itself, so it stays free of any provider dependency.

When there is nothing older than the protected tail (the whole conversation already fits in keep_last), no summarization call is made and the conversation is returned unchanged (a new list; messages is never mutated).

Parameters:

Name Type Description Default
client Any

Anything with a generate(prompt: str) -> str method.

required
messages list[dict]

The conversation, OpenAI message-dict format.

required
keep_last int

Trailing messages (not exchanges) to keep verbatim; see above.

2
prompt Optional[str]

Optional instruction prepended to the rendered transcript, replacing :data:DEFAULT_SUMMARIZE_PROMPT.

None

aimu.context.DEFAULT_SUMMARIZE_PROMPT module-attribute

DEFAULT_SUMMARIZE_PROMPT = 'Summarize the conversation below in a few sentences. Preserve concrete facts, decisions, and any open questions a continuation of this conversation would still need to know.'