From 0b92bcf0215876437d4eeb897ff23bec9dc9e8e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:34:40 +0000 Subject: [PATCH] refactor: remove StoreMemoryCommand and ForgetMemoryCommand, delegate to Embabel layer Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/01087ad6-e0f9-4913-a328-ff75494a706e Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com> --- FRAMEWORK_OVERVIEW.md | 21 +++-- ...nticAggregateBeanFactoryPostProcessor.java | 1 - .../agentic/commands/ForgetMemoryCommand.java | 54 ------------- .../agentic/commands/StoreMemoryCommand.java | 60 -------------- .../agentic/events/MemoryRevokedEvent.java | 7 +- .../runtime/AgenticAggregatePartition.java | 80 +------------------ .../AkcesAgenticAggregateController.java | 70 +++------------- .../runtime/MemorySlidingWindowTest.java | 18 ++--- 8 files changed, 33 insertions(+), 278 deletions(-) delete mode 100644 main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/ForgetMemoryCommand.java delete mode 100644 main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/StoreMemoryCommand.java diff --git a/FRAMEWORK_OVERVIEW.md b/FRAMEWORK_OVERVIEW.md index 53872648..00b66582 100644 --- a/FRAMEWORK_OVERVIEW.md +++ b/FRAMEWORK_OVERVIEW.md @@ -84,7 +84,7 @@ Spring AI or Embabel-powered chatbot). It differs from a regular aggregate in th | Replicas | Multiple (Kafka-partitioned) | Always **1** (singleton) | | Partition count | Configurable | Fixed at **1** per topic | | State class | Any `AggregateState` | Must also implement `MemoryAwareState` | -| Built-in commands | None | `StoreMemoryCommand`, `ForgetMemoryCommand` | +| Built-in commands | None | None (memory commands handled by the Embabel layer) | | Built-in events | None | `MemoryStoredEvent`, `MemoryRevokedEvent` | | Memory system | N/A | Sliding-window memory (configurable capacity) | | External events | Via `@EventBridgeHandler` | Listens to **all** partitions directly | @@ -151,25 +151,24 @@ public record MyAssistantState( } ``` -### Built-in memory commands and events +### Built-in memory events -The framework automatically handles: +The framework automatically registers and handles the following built-in events. These are +produced internally by the Embabel layer when the agent stores or revokes memories: -| Command / Event | Type | Description | -|-----------------|------|-------------| -| `StoreMemoryCommand` | Command | Asks the aggregate to store a new memory entry | -| `ForgetMemoryCommand` | Command | Asks the aggregate to remove an existing memory entry | +| Event | Type | Description | +|-------|------|-------------| | `MemoryStoredEvent` | DomainEvent | Emitted when a memory entry is stored | -| `MemoryRevokedEvent` | DomainEvent | Emitted when a memory entry is removed (either by `ForgetMemoryCommand` or sliding-window eviction) | +| `MemoryRevokedEvent` | DomainEvent | Emitted when a memory entry is removed (by the Embabel agent layer) | -You do **not** need to implement command handlers for these; the +You do **not** need to implement event-sourcing handlers for these; the `KafkaAgenticAggregateRuntime` registers them as built-ins. ### Sliding-window memory When the number of stored memories exceeds `maxMemories`, the oldest entry is automatically evicted -and a `MemoryRevokedEvent` is emitted. This keeps memory consumption bounded while preserving the -most recent context. +and a `MemoryRevokedEvent` is emitted by the Embabel agent layer. This keeps memory consumption +bounded while preserving the most recent context. ### Listening to external events diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/beans/AgenticAggregateBeanFactoryPostProcessor.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/beans/AgenticAggregateBeanFactoryPostProcessor.java index dd5ecb0e..77b92201 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/beans/AgenticAggregateBeanFactoryPostProcessor.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/beans/AgenticAggregateBeanFactoryPostProcessor.java @@ -184,7 +184,6 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) .addConstructorArgReference("agenticServiceConsumerFactory") .addConstructorArgReference("agenticServiceProducerFactory") .addConstructorArgReference("agenticServiceAggregateStateRepositoryFactory") - .addConstructorArgValue(agenticInfo.maxMemories()) .setInitMethodName("start") .setDestroyMethodName("close") .getBeanDefinition()); diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/ForgetMemoryCommand.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/ForgetMemoryCommand.java deleted file mode 100644 index a157385a..00000000 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/ForgetMemoryCommand.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2022 - 2026 The Original Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.elasticsoftware.akces.agentic.commands; - -import jakarta.annotation.Nonnull; -import org.elasticsoftware.akces.annotations.AggregateIdentifier; -import org.elasticsoftware.akces.annotations.CommandInfo; -import org.elasticsoftware.akces.commands.Command; - -/** - * Command to forget (revoke) a previously stored memory entry for an - * {@link org.elasticsoftware.akces.aggregate.AgenticAggregate}. - * - *
When handled, this command produces a - * {@link org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent} removing the memory - * identified by {@code memoryId} from the aggregate's memory store. - * - * @param agenticAggregateId the unique identifier of the target AgenticAggregate instance - * @param memoryId the UUID of the memory entry to forget - * @param reason the reason for forgetting this memory entry - */ -@CommandInfo(type = "ForgetMemory", version = 1) -public record ForgetMemoryCommand( - @AggregateIdentifier String agenticAggregateId, - String memoryId, - String reason -) implements Command { - - /** - * Returns the aggregate identifier for routing. - * - * @return the {@code agenticAggregateId} - */ - @Override - @Nonnull - public String getAggregateId() { - return agenticAggregateId; - } -} diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/StoreMemoryCommand.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/StoreMemoryCommand.java deleted file mode 100644 index b078cae5..00000000 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/commands/StoreMemoryCommand.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2022 - 2026 The Original Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.elasticsoftware.akces.agentic.commands; - -import jakarta.annotation.Nonnull; -import org.elasticsoftware.akces.annotations.AggregateIdentifier; -import org.elasticsoftware.akces.annotations.CommandInfo; -import org.elasticsoftware.akces.commands.Command; - -/** - * Command to store a new memory entry for an {@link org.elasticsoftware.akces.aggregate.AgenticAggregate}. - * - *
When handled, this command produces a - * {@link org.elasticsoftware.akces.agentic.events.MemoryStoredEvent} and, if the - * {@link org.elasticsoftware.akces.annotations.AgenticAggregateInfo#maxMemories()} sliding-window - * capacity is reached, a subsequent - * {@link org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent} is automatically emitted - * to evict the oldest memory entry. - * - * @param agenticAggregateId the unique identifier of the target AgenticAggregate instance - * @param subject a short (1–2 word) topic label for the memory being stored - * @param fact the fact to remember (max 200 characters) - * @param citations the source of the fact (e.g., a file path and line number) - * @param reason the reason for storing this fact - */ -@CommandInfo(type = "StoreMemory", version = 1) -public record StoreMemoryCommand( - @AggregateIdentifier String agenticAggregateId, - String subject, - String fact, - String citations, - String reason -) implements Command { - - /** - * Returns the aggregate identifier for routing. - * - * @return the {@code agenticAggregateId} - */ - @Override - @Nonnull - public String getAggregateId() { - return agenticAggregateId; - } -} diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryRevokedEvent.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryRevokedEvent.java index f3f3c03b..c3599c94 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryRevokedEvent.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryRevokedEvent.java @@ -28,10 +28,9 @@ * Domain event produced when a memory entry has been revoked (forgotten) from an * {@link org.elasticsoftware.akces.aggregate.AgenticAggregate}. * - *
This event is produced either explicitly via a - * {@link org.elasticsoftware.akces.agentic.commands.ForgetMemoryCommand}, or automatically - * by the {@link org.elasticsoftware.akces.agentic.runtime.AgenticAggregatePartition} as part - * of the sliding-window eviction mechanism when + *
This event is produced internally by the Embabel layer (via the agent's memory + * management tools) when a memory is explicitly revoked, or as part of the + * sliding-window eviction mechanism when * {@link org.elasticsoftware.akces.annotations.AgenticAggregateInfo#maxMemories()} is exceeded. * * @param agenticAggregateId the unique identifier of the AgenticAggregate instance diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AgenticAggregatePartition.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AgenticAggregatePartition.java index aff73cce..7a70f9ab 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AgenticAggregatePartition.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AgenticAggregatePartition.java @@ -30,8 +30,6 @@ import org.apache.kafka.common.errors.InterruptException; import org.apache.kafka.common.errors.WakeupException; import org.elasticsoftware.akces.agentic.AgenticAggregateRuntime; -import org.elasticsoftware.akces.agentic.commands.ForgetMemoryCommand; -import org.elasticsoftware.akces.aggregate.AgenticAggregateMemory; import org.elasticsoftware.akces.aggregate.DomainEventType; import org.elasticsoftware.akces.aggregate.IndexParams; import org.elasticsoftware.akces.commands.Command; @@ -81,11 +79,6 @@ *
If the number of memories in the loaded state exceeds {@code maxMemories}, a - * {@link ForgetMemoryCommand} is issued through the normal aggregate command path for - * each excess entry (oldest first) until the count is within the allowed window. - * This avoids rebuilding a separate in-memory deque after restarts. - * - *
A safety bound of {@code 2 * (maxMemories + 1)} iterations guards against a pathological
- * case where eviction commands fail to reduce the memory count (e.g., the ForgetMemory
- * handler is not registered), preventing an infinite loop.
- *
- * @param aggregateId the aggregate whose memory count should be checked
- * @param tenantId tenant identifier propagated from the triggering command
- * @param correlationId correlation identifier propagated from the triggering command
- */
- private void enforceMemorySlidingWindow(String aggregateId, String tenantId,
- String correlationId) {
- try {
- // Safety bound: evict at most 2 * (maxMemories + 1) times to prevent an infinite loop
- // if the ForgetMemory handler does not actually reduce the memory count.
- int maxEvictions = 2 * (maxMemories + 1);
- for (int evictions = 0; evictions < maxEvictions; evictions++) {
- AggregateStateRecord stateRecord = stateRepository.get(aggregateId);
- List Responsibilities:
* The record includes both the built-in agentic command types
- * ({@link StoreMemoryCommand}, {@link ForgetMemoryCommand}) and any additional command
- * types declared by the wrapped aggregate.
*/
private void publishControlRecord() {
String transactionalId = aggregateRuntime.getName() + "-" + HostUtils.getHostName()
+ "-agentic-control";
try (Producer Built-in agentic commands ({@link StoreMemoryCommand}, {@link ForgetMemoryCommand})
- * are returned directly. All other commands are resolved from the underlying
+ * Command types are resolved from the underlying
* {@link AgenticAggregateRuntime}'s local command types.
*
* @param commandClass the command class to resolve
@@ -575,13 +532,6 @@ public void setEnvironment(Environment env) {
@Override
@Nonnull
public CommandType> resolveType(@Nonnull Class extends Command> commandClass) {
- // Check built-in types first
- for (CommandType> builtIn : BUILTIN_COMMAND_TYPES) {
- if (builtIn.typeClass().equals(commandClass)) {
- return builtIn;
- }
- }
- // Delegate to aggregate runtime
var commandInfo = commandClass.getAnnotation(
org.elasticsoftware.akces.annotations.CommandInfo.class);
if (commandInfo != null) {
diff --git a/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/MemorySlidingWindowTest.java b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/MemorySlidingWindowTest.java
index 0dfd4fb5..ed9e5256 100644
--- a/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/MemorySlidingWindowTest.java
+++ b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/MemorySlidingWindowTest.java
@@ -32,15 +32,13 @@
import static org.assertj.core.api.Assertions.assertThat;
/**
- * Tests the sliding-window memory eviction logic that occurs when the memory list exceeds
- * the configured {@code maxMemories} limit. The eviction algorithm is exercised by
- * simulating the sequence of events that would be produced by
- * {@link AgenticAggregatePartition#enforceMemorySlidingWindow}.
+ * Tests the sliding-window memory eviction logic in the state transition layer.
+ * The eviction algorithm is exercised by simulating the sequence of events that are
+ * produced by the Embabel layer during the agent's memory management process.
*
- * The partition enforces the limit by issuing {@link org.elasticsoftware.akces.agentic.commands.ForgetMemoryCommand}
- * for the oldest entry (first in the list) until the count drops to the allowed window.
- * Each {@code ForgetMemoryCommand} produces a {@link MemoryRevokedEvent} that updates
- * the state through the built-in event-sourcing handler in
+ * Memory revocation is handled by the Embabel agent (via its memory management tools),
+ * which produces {@link MemoryRevokedEvent} directly. Each such event updates the
+ * state through the built-in event-sourcing handler in
* {@link KafkaAgenticAggregateRuntime}.
*/
class MemorySlidingWindowTest {
@@ -184,13 +182,13 @@ void multipleOverLimitShouldEvictMultipleOldestMemories() {
}
// -------------------------------------------------------------------------
- // ForgetMemoryCommand removes specific memory by ID
+ // Revoke memory by ID removes specific memory
// -------------------------------------------------------------------------
@Test
void forgetMemoryByIdShouldRemoveSpecificMemory() {
AggregateState state = storeMemories(5);
- // Simulate ForgetMemoryCommand for m3
+ // Simulate revoking memory m3 via MemoryRevokedEvent (produced by Embabel layer)
MemoryRevokedEvent revokeEvent = new MemoryRevokedEvent(
"agg-1", "m3", "no longer relevant", Instant.now());
state = KafkaAgenticAggregateRuntime.onMemoryRevoked(revokeEvent, state);
*