Coverage for haystack/hooks/compaction/types/protocol.py: 100%
12 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from typing import Any, Protocol
7from haystack.core.serialization import default_from_dict
8from haystack.dataclasses import ChatMessage
9from haystack.token_counters import TokenCounter
12class Compactor(Protocol):
13 """
14 Rewrites an Agent's conversation into a shorter one that carries the same working context.
16 A compactor is the *how* of context compaction; deciding *when* to compact is the caller's job, which
17 `CompactionHook` does by comparing the context size against a fraction of the model's window. Strategies
18 differ widely in cost and fidelity, from dropping the oldest messages outright to condensing them with an LLM.
20 Implementations must honor three rules:
22 1. **Return `None` unless the conversation actually gets smaller.** Callers apply whatever else is returned, so
23 judging whether compacting was worthwhile is the compactor's job.
24 2. **Return a new list; leave `messages` as it is.** The caller owns that list and writes the returned one back.
25 3. **Keep tool calls and their results together.** Do not retain a tool result after removing the assistant message
26 that contains its originating call, or retain a tool call without all of its results. Chat-completion APIs reject
27 these incomplete tool-call exchanges.
29 `target_tokens` is a goal, not a guarantee: a compactor that cannot reach it should get as close as it can rather
30 than strip the conversation past what the Agent needs to keep working.
32 Implement `to_dict` so the compactor's settings survive serialization. The default `from_dict` passes them straight
33 back to the constructor, which is enough for plain values; override it when `to_dict` emitted something that has to
34 be rebuilt first, such as a `Secret` or a nested component.
35 """
37 def compact(
38 self, messages: list[ChatMessage], target_tokens: int, token_counter: TokenCounter
39 ) -> list[ChatMessage] | None:
40 """
41 Return a shorter replacement for `messages`, or None to leave it unchanged.
43 :param messages: The conversation to compact, oldest to newest.
44 :param target_tokens: The size the compacted conversation should come in under.
45 :param token_counter: The `TokenCounter` to measure messages with. The same one the caller sized the context
46 with, so a compactor's measurements are consistent with the decision to compact.
47 :returns: The replacement conversation, or None when this compactor has nothing to change.
48 """
49 ...
51 async def compact_async(
52 self, messages: list[ChatMessage], target_tokens: int, token_counter: TokenCounter
53 ) -> list[ChatMessage] | None:
54 """
55 Asynchronously return a shorter replacement for `messages`, or None to leave it unchanged.
57 The default implementation calls `compact` directly. Override it when compaction does I/O, so the event loop is
58 not blocked.
60 :param messages: The conversation to compact, oldest to newest.
61 :param target_tokens: The size the compacted conversation should come in under.
62 :param token_counter: The `TokenCounter` to measure messages with. The same one the caller sized the context
63 with, so a compactor's measurements are consistent with the decision to compact.
64 :returns: The replacement conversation, or None when this compactor has nothing to change.
65 """
66 return self.compact(messages, target_tokens, token_counter)
68 def to_dict(self) -> dict[str, Any]:
69 """Serialize the compactor to a dictionary."""
70 ...
72 @classmethod
73 def from_dict(cls, data: dict[str, Any]) -> "Compactor":
74 """Deserialize the compactor from a dictionary."""
75 return default_from_dict(cls, data)