Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/bundles/ai-bundle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,98 @@ When using a service reference, the memory service must implement the
}
}

History Compression
-------------------

When building conversational agents, the message history can grow large over time, increasing token costs and potentially
exceeding context limits. The AI Bundle supports history compression through configurable strategies.

Configuring Compression
~~~~~~~~~~~~~~~~~~~~~~~

To enable history compression, reference a compression strategy service in your agent configuration:

.. code-block:: yaml

ai:
agent:
my_agent:
model: 'gpt-4o-mini'
compression: 'App\Compression\MyCompressionStrategy'

The referenced service must implement :class:`Symfony\\AI\\Agent\\Compression\\CompressionStrategyInterface`.

Built-in Compression Strategies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Symfony AI provides several built-in compression strategies that you can register as services:

**Sliding Window Strategy**: Keeps only the most recent messages, discarding older ones::

use Symfony\AI\Agent\Compression\SlidingWindowStrategy;

// In services.yaml
services:
Symfony\AI\Agent\Compression\SlidingWindowStrategy:
$maxMessages: 10 # Keep last 10 messages after compression
$threshold: 20 # Trigger compression when exceeding 20 messages

**Summarization Strategy**: Uses an LLM to summarize older messages while keeping recent ones intact::

use Symfony\AI\Agent\Compression\SummarizationStrategy;

// In services.yaml
services:
Symfony\AI\Agent\Compression\SummarizationStrategy:
$platform: '@ai.platform.openai'
$summaryModel: 'gpt-5-mini'
$threshold: 20
$keepRecent: 6

**Hybrid Strategy**: Combines multiple strategies with progressive thresholds::

use Symfony\AI\Agent\Compression\HybridStrategy;

// In services.yaml
services:
Symfony\AI\Agent\Compression\HybridStrategy:
$primaryStrategy: '@Symfony\AI\Agent\Compression\SlidingWindowStrategy'
$secondaryStrategy: '@Symfony\AI\Agent\Compression\SummarizationStrategy'
$softThreshold: 15
$hardThreshold: 30

Disabling Compression Per-Request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Compression can be disabled for specific agent calls using the ``compress_history`` option::

$result = $agent->call($messages, [
'compress_history' => false,
]);

Compression Events
~~~~~~~~~~~~~~~~~~

The compression processor dispatches events that allow you to monitor or modify the compression process:

* :class:`Symfony\\AI\\Agent\\Compression\\BeforeHistoryCompression`: Dispatched before compression, allows skipping
* :class:`Symfony\\AI\\Agent\\Compression\\AfterHistoryCompression`: Dispatched after compression, allows modifying the result

Example event listener::

use Symfony\AI\Agent\Compression\AfterHistoryCompression;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

#[AsEventListener]
final class CompressionLogger
{
public function __invoke(AfterHistoryCompression $event): void
{
$delta = $event->getCompressionDelta();
// Log that $delta messages were compressed
}
}

Multi-Agent Orchestration
-------------------------

Expand Down
120 changes: 120 additions & 0 deletions docs/components/agent.rst
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,126 @@ useful when certain interactions shouldn't be influenced by the memory context::
]);


History Compression
-------------------

When building conversational agents, the message history can grow large over time, increasing token costs and potentially
exceeding context limits. Symfony AI provides history compression strategies to manage conversation length automatically.

Using History Compression
^^^^^^^^^^^^^^^^^^^^^^^^^

History compression is handled through the :class:`Symfony\\AI\\Agent\\Compression\\HistoryCompressionInputProcessor` and a
compression strategy implementing :class:`Symfony\\AI\\Agent\\Compression\\CompressionStrategyInterface`::

use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\Compression\HistoryCompressionInputProcessor;
use Symfony\AI\Agent\Compression\SlidingWindowStrategy;

// Platform instantiation

$strategy = new SlidingWindowStrategy(maxMessages: 10, threshold: 20);
$compressionProcessor = new HistoryCompressionInputProcessor($strategy);

$agent = new Agent($platform, $model, [$compressionProcessor]);

Compression Strategies
^^^^^^^^^^^^^^^^^^^^^^

The library includes several compression strategy implementations.

Sliding Window Strategy
.......................

The simplest strategy that keeps only the most recent messages, discarding older ones. The system message is always
preserved::

use Symfony\AI\Agent\Compression\SlidingWindowStrategy;

$strategy = new SlidingWindowStrategy(
maxMessages: 10, // Keep last 10 messages after compression
threshold: 20, // Trigger compression when exceeding 20 messages
);

Summarization Strategy
......................

This strategy uses an LLM to summarize older messages while keeping recent messages intact. The summary is injected
into the system message::

use Symfony\AI\Agent\Compression\SummarizationStrategy;

$strategy = new SummarizationStrategy(
$platform,
'gemini-2.0-flash', // Model to use for summarization
threshold: 20, // Trigger compression when exceeding 20 messages
keepRecent: 6, // Keep the 6 most recent messages uncompressed
);

Hybrid Strategy
...............

Combines multiple strategies with progressive thresholds. This allows using a lightweight strategy for moderate
conversation lengths and a more aggressive strategy for very long conversations::

use Symfony\AI\Agent\Compression\HybridStrategy;
use Symfony\AI\Agent\Compression\SlidingWindowStrategy;
use Symfony\AI\Agent\Compression\SummarizationStrategy;

$slidingWindow = new SlidingWindowStrategy(maxMessages: 15);
$summarization = new SummarizationStrategy($platform, 'gemini-2.0-flash', keepRecent: 6);

$strategy = new HybridStrategy(
primaryStrategy: $slidingWindow, // Used between soft and hard threshold
secondaryStrategy: $summarization, // Used above hard threshold
softThreshold: 15, // When to start using primary strategy
hardThreshold: 30, // When to switch to secondary strategy
);

Dynamic Compression Control
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Compression is globally configured for the agent, but you can selectively disable it for specific calls::

$result = $agent->call($messages, [
'compress_history' => false, // Disable compression for this specific call
]);

Compression Events
^^^^^^^^^^^^^^^^^^

The compression processor dispatches events that allow you to monitor or modify the compression process.

BeforeHistoryCompression
........................

Dispatched before compression is applied. Listeners can inspect the original messages and skip compression::

use Symfony\AI\Agent\Compression\BeforeHistoryCompression;

$eventDispatcher->addListener(BeforeHistoryCompression::class, function (BeforeHistoryCompression $event): void {
// Optionally skip compression based on custom logic
if ($this->shouldPreserveFullHistory($event->getOriginalMessages())) {
$event->skip();
}
});

AfterHistoryCompression
.......................

Dispatched after compression is applied. Listeners can inspect or modify the compressed messages::

use Symfony\AI\Agent\Compression\AfterHistoryCompression;

$eventDispatcher->addListener(AfterHistoryCompression::class, function (AfterHistoryCompression $event): void {
$delta = $event->getCompressionDelta();
$this->logger->info(sprintf('Compressed %d messages', $delta));

// Optionally modify the compressed messages
$event->setCompressedMessages($modifiedMessages);
});


Testing
-------

Expand Down
1 change: 1 addition & 0 deletions src/agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
---

* [BC BREAK] Rename `Symfony\AI\Agent\Toolbox\Tool\Agent` to `Symfony\AI\Agent\Toolbox\Tool\Subagent`
* Add history compression strategies for managing conversation length including events for compression lifecycle

0.3
---
Expand Down
55 changes: 55 additions & 0 deletions src/agent/src/Compression/AfterHistoryCompression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Agent\Compression;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move it to Event?


use Symfony\AI\Platform\Message\MessageBag;

/**
* Event dispatched after history compression is applied.
* Listeners can inspect or modify the compressed messages before they are used.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class AfterHistoryCompression
{
public function __construct(
private readonly MessageBag $originalMessages,
private MessageBag $compressedMessages,
) {
}

public function getOriginalMessages(): MessageBag
{
return $this->originalMessages;
}

public function getCompressedMessages(): MessageBag
{
return $this->compressedMessages;
}

/**
* Replace the compressed messages (e.g., for further modification).
*/
public function setCompressedMessages(MessageBag $messages): void
{
$this->compressedMessages = $messages;
}

/**
* Returns the number of messages removed by compression.
*/
public function getCompressionDelta(): int
{
return \count($this->originalMessages) - \count($this->compressedMessages);
}
}
48 changes: 48 additions & 0 deletions src/agent/src/Compression/BeforeHistoryCompression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Agent\Compression;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same


use Symfony\AI\Platform\Message\MessageBag;

/**
* Event dispatched before history compression is applied.
* Listeners can inspect the original messages and optionally skip compression.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class BeforeHistoryCompression
{
private bool $skip = false;

public function __construct(
private readonly MessageBag $originalMessages,
) {
}

public function getOriginalMessages(): MessageBag
{
return $this->originalMessages;
}

/**
* Call this to prevent compression from being applied.
*/
public function skip(): void
{
$this->skip = true;
}

public function isSkipped(): bool
{
return $this->skip;
}
}
32 changes: 32 additions & 0 deletions src/agent/src/Compression/CompressionStrategyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\AI\Agent\Compression;

use Symfony\AI\Platform\Message\MessageBag;

/**
* Strategy for compressing conversation history.
*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
interface CompressionStrategyInterface
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interface seems more generic than it's name would suggest, it's more like MessageBagTransformer.

Reason I'm saying this is that there could be use cases other than compression for using this, especially around filtering/adding more messages.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One use case might be to remove certain types of messages that are not supported by other models/agents during hand-off/fork.

{
/**
* Determines whether the message bag should be compressed.
*/
public function shouldCompress(MessageBag $messages): bool;

/**
* Compresses the message bag and returns a new, smaller message bag.
*/
public function compress(MessageBag $messages): MessageBag;
}
Loading