From 7d8a04429b721b97c33c8974640eab048f565584 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:44:29 +0000 Subject: [PATCH 1/7] Initial plan From a9f3b057a264bfd8b1ccded7f9b8da7602d70715 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 05:58:42 +0000 Subject: [PATCH 2/7] =?UTF-8?q?feat(embabel):=20Phase=202=20=E2=80=94=20Ak?= =?UTF-8?q?cesAgentComponent=20with=20Goals,=20Actions,=20and=20Conditions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create new @EmbabelComponent class with reusable memory management capabilities for Akces-based agentic systems: - StoreMemoryAction: @Action producing MemoryStoredEvent directly - ForgetMemoryAction: @Action producing MemoryRevokedEvent directly - RecallMemoriesAction: @Action(readOnly=true) filtering memories - HasMemoriesCondition: @Condition evaluating memory presence - LearnFromProcessGoal: @AchievesGoal for multi-step LLM reasoning - MemoryLearningResult: output record for the learning goal Includes 32 unit tests covering all actions, conditions, goals, annotation verification, and edge cases. Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/08a28efe-4c64-42df-a90a-8c6192fafa4f Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com> --- .../agentic/agent/AkcesAgentComponent.java | 300 ++++++++++++ .../agentic/agent/MemoryLearningResult.java | 39 ++ .../agent/AkcesAgentComponentTest.java | 426 ++++++++++++++++++ 3 files changed, 765 insertions(+) create mode 100644 main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponent.java create mode 100644 main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java create mode 100644 main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponent.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponent.java new file mode 100644 index 00000000..94279023 --- /dev/null +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponent.java @@ -0,0 +1,300 @@ +/* + * 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.agent; + +import com.embabel.agent.api.annotation.AchievesGoal; +import com.embabel.agent.api.annotation.Action; +import com.embabel.agent.api.annotation.Condition; +import com.embabel.agent.api.annotation.EmbabelComponent; +import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent; +import org.elasticsoftware.akces.agentic.events.MemoryStoredEvent; +import org.elasticsoftware.akces.aggregate.AgenticAggregateMemory; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +/** + * Central Embabel component that provides reusable Goals, Actions, and Conditions for + * Akces-based agentic systems. + * + *
This class is annotated with {@link EmbabelComponent} and is automatically discovered + * by the {@code AgentPlatform} via component scanning. It provides the foundational agent + * capabilities for memory management and knowledge learning that all agentic aggregates + * share. + * + *
Reads the aggregate identifier from the blackboard and accepts the memory + * content fields ({@code subject}, {@code fact}, {@code citations}, {@code reason}) + * as inputs — typically generated by the LLM during the learning process. The action + * produces a {@link MemoryStoredEvent} directly (bypassing the internal command path) + * which is collected by the partition and applied through the built-in + * {@code @EventSourcingHandler} (calling {@code MemoryAwareState.withMemory()}). + * + * @param agenticAggregateId the unique identifier of the agentic aggregate instance, + * read from the blackboard + * @param subject a short (1–3 word) topic label for the memory + * (e.g. "error handling", "market patterns") + * @param fact a clear, concise factual statement (max ~200 characters) + * @param citations source reference — the command/event type that triggered + * this learning, or relevant data points + * @param reason why this fact is worth remembering — what future decisions + * it informs (2–3 sentences) + * @return a {@link MemoryStoredEvent} containing all memory metadata + */ + @Action(description = "Store a learned fact as a memory entry for the agentic aggregate") + public MemoryStoredEvent storeMemory( + String agenticAggregateId, + String subject, + String fact, + String citations, + String reason) { + return new MemoryStoredEvent( + agenticAggregateId, + UUID.randomUUID().toString(), + subject, + fact, + citations, + reason, + Instant.now()); + } + + /** + * Revokes (forgets) a memory entry that is no longer relevant or accurate. + * + *
Takes the {@code memoryId} of the entry to revoke and a {@code reason} from the + * blackboard, reads the aggregate identifier, and produces a + * {@link MemoryRevokedEvent}. The event is applied through the built-in + * {@code @EventSourcingHandler} (calling {@code MemoryAwareState.withoutMemory()}). + * + *
This action enables agent self-management of its knowledge base: the agent can + * correct its knowledge by removing outdated or incorrect facts, and can enforce + * memory capacity limits by evicting the oldest entries. + * + * @param agenticAggregateId the unique identifier of the agentic aggregate instance, + * read from the blackboard + * @param memoryId the UUID of the memory entry to revoke + * @param reason the reason the memory is being revoked + * @return a {@link MemoryRevokedEvent} for the identified memory entry + */ + @Action(description = "Revoke a memory entry that is no longer relevant or accurate") + public MemoryRevokedEvent forgetMemory( + String agenticAggregateId, + String memoryId, + String reason) { + return new MemoryRevokedEvent( + agenticAggregateId, + memoryId, + reason, + Instant.now()); + } + + /** + * Searches stored memories by subject or keyword to retrieve relevant facts. + * + *
Reads the current {@code List This action is declared {@code readOnly = true} since it has no side effects —
+ * it only inspects the current memory list without modifying state.
+ *
+ * @param memories the current list of memories from the blackboard; may be
+ * {@code null} or empty
+ * @param query a search term to filter memories by subject, fact, or reason;
+ * if {@code null} or blank, all memories are returned
+ * @return an unmodifiable list of matching memories; never {@code null}
+ */
+ @Action(description = "Search stored memories by subject or keyword to retrieve relevant facts",
+ readOnly = true)
+ public List This condition is used as a precondition for actions that depend on prior
+ * knowledge. For example, "recall memories" is only meaningful when memories exist.
+ *
+ * @param memories the current list of memories from the blackboard; may be
+ * {@code null} or empty
+ * @return {@code true} if at least one memory is present; {@code false} otherwise
+ */
+ @Condition(name = "hasMemories")
+ public boolean hasMemories(List This method is annotated with both {@link AchievesGoal} and {@link Action}.
+ * The {@code @AchievesGoal} annotation declares the "LearnFromProcess" goal in the
+ * Embabel GOAP planner, and the planner orchestrates a multi-step reasoning pipeline
+ * using the available actions:
+ * Constraints enforced by the LLM planner:
+ * The match is case-insensitive and checks the memory's {@code subject},
+ * {@code fact}, and {@code reason} fields.
+ *
+ * @param memory the memory entry to check
+ * @param lowerQuery the search query in lowercase
+ * @return {@code true} if any of the checked fields contain the query
+ */
+ private static boolean matchesQuery(AgenticAggregateMemory memory, String lowerQuery) {
+ return containsIgnoreCase(memory.subject(), lowerQuery)
+ || containsIgnoreCase(memory.fact(), lowerQuery)
+ || containsIgnoreCase(memory.reason(), lowerQuery);
+ }
+
+ /**
+ * Null-safe, case-insensitive substring check.
+ *
+ * @param text the text to search within; may be {@code null}
+ * @param lower the lowercase search term
+ * @return {@code true} if {@code text} contains {@code lower} (case-insensitive)
+ */
+ private static boolean containsIgnoreCase(String text, String lower) {
+ return text != null && text.toLowerCase().contains(lower);
+ }
+}
diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java
new file mode 100644
index 00000000..fe0f3bf5
--- /dev/null
+++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java
@@ -0,0 +1,39 @@
+/*
+ * 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.agent;
+
+/**
+ * Result object produced by the {@code LearnFromProcessGoal} when the learning
+ * cycle completes.
+ *
+ * This record is the output type of the
+ * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} goal action and serves
+ * as a structured summary of the memory management operations performed during a single
+ * agent process execution.
+ *
+ * @param memoriesStored the number of new memories stored during this learning cycle
+ * @param memoriesRevoked the number of memories revoked (evicted or replaced) during
+ * this learning cycle
+ * @param summary a human-readable summary of what was learned and any capacity
+ * management actions taken
+ */
+public record MemoryLearningResult(
+ int memoriesStored,
+ int memoriesRevoked,
+ String summary
+) {}
diff --git a/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
new file mode 100644
index 00000000..19604365
--- /dev/null
+++ b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
@@ -0,0 +1,426 @@
+/*
+ * 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.agent;
+
+import com.embabel.agent.api.annotation.Action;
+import com.embabel.agent.api.annotation.AchievesGoal;
+import com.embabel.agent.api.annotation.Condition;
+import com.embabel.agent.api.annotation.EmbabelComponent;
+import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent;
+import org.elasticsoftware.akces.agentic.events.MemoryStoredEvent;
+import org.elasticsoftware.akces.aggregate.AgenticAggregateMemory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Method;
+import java.time.Instant;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Unit tests for {@link AkcesAgentComponent}, verifying that all Actions, Conditions,
+ * and Goals produce correct outputs and are properly annotated for Embabel discovery.
+ *
+ * Tests cover:
+ * This method is annotated with both {@link AchievesGoal} and {@link Action}.
* The {@code @AchievesGoal} annotation declares the "LearnFromProcess" goal in the
- * Embabel GOAP planner, and the planner orchestrates a multi-step reasoning pipeline
- * using the available actions:
+ * Embabel GOAP planner. The method uses {@link OperationContext#ai()} to invoke LLM
+ * reasoning over the current session context (command/event, aggregate state,
+ * produced events, existing memories, aggregate service records) to determine what
+ * facts are worth remembering.
+ *
+ * The planner orchestrates a multi-step reasoning pipeline using the available
+ * actions:
* Constraints enforced by the LLM planner:
+ * Constraints enforced by the LLM:
* The match is case-insensitive and checks the memory's {@code subject},
- * {@code fact}, and {@code reason} fields.
+ * The match is case-insensitive (locale-independent) and checks the memory's
+ * {@code subject}, {@code fact}, and {@code reason} fields.
*
- * @param memory the memory entry to check
- * @param lowerQuery the search query in lowercase
+ * @param memory the memory entry to check
+ * @param query the search query
* @return {@code true} if any of the checked fields contain the query
*/
- private static boolean matchesQuery(AgenticAggregateMemory memory, String lowerQuery) {
- return containsIgnoreCase(memory.subject(), lowerQuery)
- || containsIgnoreCase(memory.fact(), lowerQuery)
- || containsIgnoreCase(memory.reason(), lowerQuery);
+ private static boolean matchesQuery(AgenticAggregateMemory memory, String query) {
+ return containsIgnoreCase(memory.subject(), query)
+ || containsIgnoreCase(memory.fact(), query)
+ || containsIgnoreCase(memory.reason(), query);
}
/**
- * Null-safe, case-insensitive substring check.
+ * Null-safe, locale-independent, case-insensitive substring check using
+ * {@link String#regionMatches(boolean, int, String, int, int)}.
*
* @param text the text to search within; may be {@code null}
- * @param lower the lowercase search term
- * @return {@code true} if {@code text} contains {@code lower} (case-insensitive)
+ * @param query the search term
+ * @return {@code true} if {@code text} contains {@code query} (case-insensitive)
*/
- private static boolean containsIgnoreCase(String text, String lower) {
- return text != null && text.toLowerCase().contains(lower);
+ private static boolean containsIgnoreCase(String text, String query) {
+ if (text == null) {
+ return false;
+ }
+ int searchLength = query.length();
+ int maxStart = text.length() - searchLength;
+ for (int i = 0; i <= maxStart; i++) {
+ if (text.regionMatches(true, i, query, 0, searchLength)) {
+ return true;
+ }
+ }
+ return false;
}
}
diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java
index fe0f3bf5..d512fac9 100644
--- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java
+++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java
@@ -18,11 +18,11 @@
package org.elasticsoftware.akces.agentic.agent;
/**
- * Result object produced by the {@code LearnFromProcessGoal} when the learning
- * cycle completes.
+ * Result object produced by the {@link AkcesAgentComponent#learnFromProcess
+ * learnFromProcess} action when the learning cycle completes.
*
* This record is the output type of the
- * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} goal action and serves
+ * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} action and serves
* as a structured summary of the memory management operations performed during a single
* agent process execution.
*
diff --git a/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
index 19604365..6f94e2fa 100644
--- a/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
+++ b/main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
@@ -21,6 +21,9 @@
import com.embabel.agent.api.annotation.AchievesGoal;
import com.embabel.agent.api.annotation.Condition;
import com.embabel.agent.api.annotation.EmbabelComponent;
+import com.embabel.agent.api.common.Ai;
+import com.embabel.agent.api.common.OperationContext;
+import com.embabel.agent.api.common.PromptRunner;
import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent;
import org.elasticsoftware.akces.agentic.events.MemoryStoredEvent;
import org.elasticsoftware.akces.aggregate.AgenticAggregateMemory;
@@ -28,11 +31,14 @@
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import java.lang.reflect.Method;
import java.time.Instant;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
/**
* Unit tests for {@link AkcesAgentComponent}, verifying that all Actions, Conditions,
@@ -63,7 +69,7 @@ void setUp() {
}
// =========================================================================
- // Annotation verification
+ // Annotation presence verification
// =========================================================================
@Nested
@@ -78,67 +84,41 @@ void classShouldBeAnnotatedWithEmbabelComponent() {
@Test
void storeMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
- Method method = AkcesAgentComponent.class.getMethod(
+ var method = AkcesAgentComponent.class.getMethod(
"storeMemory", String.class, String.class, String.class,
String.class, String.class);
- Action action = method.getAnnotation(Action.class);
-
- assertThat(action).isNotNull();
- assertThat(action.description())
- .isEqualTo("Store a learned fact as a memory entry for the agentic aggregate");
- assertThat(action.readOnly()).isFalse();
+ assertThat(method.isAnnotationPresent(Action.class)).isTrue();
}
@Test
void forgetMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
- Method method = AkcesAgentComponent.class.getMethod(
+ var method = AkcesAgentComponent.class.getMethod(
"forgetMemory", String.class, String.class, String.class);
- Action action = method.getAnnotation(Action.class);
-
- assertThat(action).isNotNull();
- assertThat(action.description())
- .isEqualTo("Revoke a memory entry that is no longer relevant or accurate");
- assertThat(action.readOnly()).isFalse();
+ assertThat(method.isAnnotationPresent(Action.class)).isTrue();
}
@Test
void recallMemoriesShouldBeAnnotatedWithReadOnlyAction() throws NoSuchMethodException {
- Method method = AkcesAgentComponent.class.getMethod(
+ var method = AkcesAgentComponent.class.getMethod(
"recallMemories", List.class, String.class);
Action action = method.getAnnotation(Action.class);
-
assertThat(action).isNotNull();
- assertThat(action.description()).contains("Search stored memories");
- assertThat(action.readOnly())
- .as("recallMemories must be readOnly")
- .isTrue();
+ assertThat(action.readOnly()).as("recallMemories must be readOnly").isTrue();
}
@Test
void hasMemoriesShouldBeAnnotatedWithCondition() throws NoSuchMethodException {
- Method method = AkcesAgentComponent.class.getMethod("hasMemories", List.class);
- Condition condition = method.getAnnotation(Condition.class);
-
- assertThat(condition).isNotNull();
- assertThat(condition.name()).isEqualTo("hasMemories");
+ var method = AkcesAgentComponent.class.getMethod("hasMemories", List.class);
+ assertThat(method.isAnnotationPresent(Condition.class)).isTrue();
}
@Test
void learnFromProcessShouldBeAnnotatedWithAchievesGoalAndAction()
throws NoSuchMethodException {
- Method method = AkcesAgentComponent.class.getMethod("learnFromProcess", List.class);
-
- AchievesGoal achievesGoal = method.getAnnotation(AchievesGoal.class);
- assertThat(achievesGoal).isNotNull();
- assertThat(achievesGoal.description())
- .contains("Analyze current session information")
- .contains("Maximum 3 new memories")
- .contains("No duplicate memories")
- .contains("Memory capacity enforcement");
-
- Action action = method.getAnnotation(Action.class);
- assertThat(action).isNotNull();
- assertThat(action.description()).contains("learning process");
+ var method = AkcesAgentComponent.class.getMethod(
+ "learnFromProcess", List.class, OperationContext.class);
+ assertThat(method.isAnnotationPresent(AchievesGoal.class)).isTrue();
+ assertThat(method.isAnnotationPresent(Action.class)).isTrue();
}
}
@@ -392,30 +372,60 @@ void shouldReturnTrueWhenMultipleMemoriesExist() {
class LearnFromProcessGoalTests {
@Test
- void shouldProduceMemoryLearningResult() {
+ void shouldInvokeLlmViaOperationContextAndReturnResult() {
+ var expectedResult = new MemoryLearningResult(2, 1, "Stored 2, revoked 1");
+ OperationContext context = mock(OperationContext.class);
+ Ai ai = mock(Ai.class);
+ PromptRunner promptRunner = mock(PromptRunner.class);
+ @SuppressWarnings("unchecked")
+ PromptRunner.Creating This class is annotated with {@link EmbabelComponent} and is automatically discovered
* by the {@code AgentPlatform} via component scanning. It provides the foundational agent
- * capabilities for memory management and knowledge learning that all agentic aggregates
- * share.
+ * capabilities for memory retrieval, condition evaluation, and LLM-powered memory
+ * distillation that all agentic aggregates share.
+ *
+ * Memory storage and revocation are handled by {@code @Tool}-annotated default methods
+ * on the {@link AgenticAggregate} interface, which are exposed to the LLM via
+ * {@code withToolObject()} during the learning goal.
*
* Reads the aggregate identifier from the blackboard and accepts the memory
- * content fields ({@code subject}, {@code fact}, {@code citations}, {@code reason})
- * as inputs — typically generated by the LLM during the learning process. The action
- * produces a {@link MemoryStoredEvent} directly (bypassing the internal command path)
- * which is collected by the partition and applied through the built-in
- * {@code @EventSourcingHandler} (calling {@code MemoryAwareState.withMemory()}).
- *
- * @param agenticAggregateId the unique identifier of the agentic aggregate instance,
- * read from the blackboard
- * @param subject a short (1–3 word) topic label for the memory
- * (e.g. "error handling", "market patterns")
- * @param fact a clear, concise factual statement (max ~200 characters)
- * @param citations source reference — the command/event type that triggered
- * this learning, or relevant data points
- * @param reason why this fact is worth remembering — what future decisions
- * it informs (2–3 sentences)
- * @return a {@link MemoryStoredEvent} containing all memory metadata
- */
- @Action(description = "Store a learned fact as a memory entry for the agentic aggregate")
- public MemoryStoredEvent storeMemory(
- String agenticAggregateId,
- String subject,
- String fact,
- String citations,
- String reason) {
- return new MemoryStoredEvent(
- agenticAggregateId,
- UUID.randomUUID().toString(),
- subject,
- fact,
- citations,
- reason,
- Instant.now());
- }
-
- /**
- * Revokes (forgets) a memory entry that is no longer relevant or accurate.
- *
- * Takes the {@code memoryId} of the entry to revoke and a {@code reason} from the
- * blackboard, reads the aggregate identifier, and produces a
- * {@link MemoryRevokedEvent}. The event is applied through the built-in
- * {@code @EventSourcingHandler} (calling {@code MemoryAwareState.withoutMemory()}).
- *
- * This action enables agent self-management of its knowledge base: the agent can
- * correct its knowledge by removing outdated or incorrect facts, and can enforce
- * memory capacity limits by evicting the oldest entries.
- *
- * @param agenticAggregateId the unique identifier of the agentic aggregate instance,
- * read from the blackboard
- * @param memoryId the UUID of the memory entry to revoke
- * @param reason the reason the memory is being revoked
- * @return a {@link MemoryRevokedEvent} for the identified memory entry
- */
- @Action(description = "Revoke a memory entry that is no longer relevant or accurate")
- public MemoryRevokedEvent forgetMemory(
- String agenticAggregateId,
- String memoryId,
- String reason) {
- return new MemoryRevokedEvent(
- agenticAggregateId,
- memoryId,
- reason,
- Instant.now());
- }
-
/**
* Searches stored memories by subject or keyword to retrieve relevant facts.
*
@@ -212,22 +141,18 @@ public boolean hasMemories(List This method is annotated with both {@link AchievesGoal} and {@link Action}.
* The {@code @AchievesGoal} annotation declares the "LearnFromProcess" goal in the
* Embabel GOAP planner. The method uses {@link OperationContext#ai()} to invoke LLM
- * reasoning over the current session context (command/event, aggregate state,
- * produced events, existing memories, aggregate service records) to determine what
- * facts are worth remembering.
+ * reasoning, passing the {@link AgenticAggregate} instance as a tool object so the
+ * LLM can call the {@code @Tool}-annotated {@code storeMemory()} and
+ * {@code forgetMemory()} methods during reasoning.
*
- * The planner orchestrates a multi-step reasoning pipeline using the available
- * actions:
- * The LLM produces a {@link MemoryLearningResult} containing:
+ * Constraints enforced by the LLM:
* This record is the output type of the
* {@link AkcesAgentComponent#learnFromProcess learnFromProcess} action and serves
* as a structured summary of the memory management operations performed during a single
- * agent process execution.
+ * agent process execution. The LLM populates the event lists by calling the
+ * {@link org.elasticsoftware.akces.aggregate.AgenticAggregate#storeMemory storeMemory}
+ * and {@link org.elasticsoftware.akces.aggregate.AgenticAggregate#forgetMemory forgetMemory}
+ * {@code @Tool} methods during the learning process.
*
- * @param memoriesStored the number of new memories stored during this learning cycle
- * @param memoriesRevoked the number of memories revoked (evicted or replaced) during
- * this learning cycle
+ * @param memoriesStored the list of {@link MemoryStoredEvent}s produced during this
+ * learning cycle
+ * @param memoriesRevoked the list of {@link MemoryRevokedEvent}s produced during this
+ * learning cycle
* @param summary a human-readable summary of what was learned and any capacity
* management actions taken
*/
public record MemoryLearningResult(
- int memoriesStored,
- int memoriesRevoked,
+ List In addition to direct {@link DomainEvent} objects on the blackboard, this class
+ * also extracts events from {@link MemoryLearningResult} objects, which contain lists
+ * of {@code MemoryStoredEvent} and {@code MemoryRevokedEvent} produced by the LLM
+ * calling {@code @Tool} methods on the {@code AgenticAggregate} during the learning goal.
+ *
* Unknown {@link ErrorEvent} types (not declared in {@code agentProducedErrors} and
* therefore not registered as {@link DomainEventType}s in the runtime) are logged at
* {@code WARN} level and excluded from the returned list. This prevents
@@ -63,6 +69,10 @@ private AgentProcessResultTranslator() {
* the subset that can safely be passed to the runtime's {@code processDomainEvent()}
* method.
*
+ * This method also extracts events from any {@link MemoryLearningResult} objects
+ * on the blackboard (which contain lists of {@code MemoryStoredEvent} and
+ * {@code MemoryRevokedEvent} produced by LLM tool calls).
+ *
* For every collected event:
* Tests cover:
* This interface adds:
+ * The {@code @Tool}-annotated default methods are exposed to the LLM during the
+ * learning goal's {@code context.ai().withToolObject(aggregate)} call, allowing the LLM
+ * to invoke them as function-call tools during memory distillation. The returned events
+ * are collected from the {@code MemoryLearningResult} and applied through the standard
+ * event-sourcing flow.
+ *
+ * @param This method is annotated with {@link Tool @Tool} and is exposed to the LLM
+ * when the aggregate instance is passed via {@code withToolObject()}. The LLM calls
+ * this tool during the learning process to persist new memories.
+ *
+ * @param agenticAggregateId the unique identifier of the agentic aggregate instance
+ * @param subject a short (1–3 word) topic label for the memory
+ * @param fact a clear, concise factual statement (max ~200 characters)
+ * @param citations source reference — the command/event type that triggered
+ * this learning, or relevant data points
+ * @param reason why this fact is worth remembering — what future decisions
+ * it informs (2–3 sentences)
+ * @return a {@link MemoryStoredEvent} containing all memory metadata
+ */
+ @Tool(description = "Store a learned fact as a memory entry for the agentic aggregate")
+ default MemoryStoredEvent storeMemory(
+ @ToolParam(description = "The unique identifier of the agentic aggregate instance")
+ String agenticAggregateId,
+ @ToolParam(description = "A short 1-3 word topic label for the memory")
+ String subject,
+ @ToolParam(description = "A clear, concise factual statement (max ~200 characters)")
+ String fact,
+ @ToolParam(description = "Source reference - the command/event type that triggered this learning")
+ String citations,
+ @ToolParam(description = "Why this fact is worth remembering - 2-3 sentences explaining significance")
+ String reason) {
+ return new MemoryStoredEvent(
+ agenticAggregateId,
+ UUID.randomUUID().toString(),
+ subject,
+ fact,
+ citations,
+ reason,
+ Instant.now());
+ }
+
+ /**
+ * Revokes (forgets) a memory entry that is no longer relevant or accurate.
+ *
+ * This method is annotated with {@link Tool @Tool} and is exposed to the LLM
+ * when the aggregate instance is passed via {@code withToolObject()}. The LLM calls
+ * this tool to remove outdated or incorrect memories, or to enforce capacity limits
+ * by evicting the oldest entries.
+ *
+ * @param agenticAggregateId the unique identifier of the agentic aggregate instance
+ * @param memoryId the UUID of the memory entry to revoke
+ * @param reason the reason the memory is being revoked
+ * @return a {@link MemoryRevokedEvent} for the identified memory entry
+ */
+ @Tool(description = "Revoke a memory entry that is no longer relevant or accurate")
+ default MemoryRevokedEvent forgetMemory(
+ @ToolParam(description = "The unique identifier of the agentic aggregate instance")
+ String agenticAggregateId,
+ @ToolParam(description = "The UUID of the memory entry to revoke")
+ String memoryId,
+ @ToolParam(description = "The reason the memory is being revoked")
+ String reason) {
+ return new MemoryRevokedEvent(
+ agenticAggregateId,
+ memoryId,
+ reason,
+ Instant.now());
+ }
}
From 8ab3e4ddb3de207bf122fb95c3132589dd5ce43d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Apr 2026 16:08:12 +0000
Subject: [PATCH 7/7] docs: fix subject field doc to say 1-3 words
consistently, update plan
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Fix MemoryStoredEvent Javadoc: "1-2 word" → "1-3 word" for consistency
- Update embabel-integration-plan.md sections 2.2, 2.3, 2.7 to reflect
storeMemory/forgetMemory as @Tool methods on AgenticAggregate,
MemoryLearningResult with event lists, and withToolObject() usage
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/f96875e9-da1c-42f6-9ee4-6680fae94c60
Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
---
.../agentic/events/MemoryStoredEvent.java | 2 +-
plans/embabel-integration-plan.md | 89 +++++++++++--------
2 files changed, 52 insertions(+), 39 deletions(-)
diff --git a/main/api/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryStoredEvent.java b/main/api/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryStoredEvent.java
index 3554ed57..2d04f192 100644
--- a/main/api/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryStoredEvent.java
+++ b/main/api/src/main/java/org/elasticsoftware/akces/agentic/events/MemoryStoredEvent.java
@@ -33,7 +33,7 @@
*
* @param agenticAggregateId the unique identifier of the AgenticAggregate instance
* @param memoryId UUID uniquely identifying this memory entry
- * @param subject a short (1–2 word) topic label for the stored memory
+ * @param subject a short (1–3 word) topic label for the stored memory
* @param fact the fact that was stored (max 200 characters)
* @param citations the source citation for the fact
* @param reason the reason the memory was stored
diff --git a/plans/embabel-integration-plan.md b/plans/embabel-integration-plan.md
index e64d751f..86a4de55 100644
--- a/plans/embabel-integration-plan.md
+++ b/plans/embabel-integration-plan.md
@@ -346,37 +346,45 @@ The resulting `AggregateServiceRecord.producedEvents` would contain:
**Why**: Embabel discovers Goals/Actions/Conditions by scanning `@EmbabelComponent`-annotated classes. This provides a centralized place for Akces-specific agent capabilities that are automatically registered with the `AgentPlatform`.
-### 2.2 Memory Management Action: `StoreMemoryAction`
+### ~~2.2 Memory Management Action: `StoreMemoryAction`~~ — MOVED TO `AgenticAggregate` INTERFACE
-**What**: An `@Action`-annotated method that produces a `MemoryStoredEvent` directly. The event is applied to the state via the built-in `@EventSourcingHandler` for `MemoryStoredEvent`.
+> **Note**: The `storeMemory` method has been moved from `AkcesAgentComponent` (as an `@Action`) to the `AgenticAggregate` interface (as a `@Tool`-annotated default method). This allows the LLM to call it directly during `learnFromProcess` via `withToolObject(aggregate)`.
-```
-@Action(description = "Store a learned fact as a memory entry for the agentic aggregate")
+**What**: A `@Tool`-annotated (Spring AI `org.springframework.ai.tool.annotation.Tool`) default method on the `AgenticAggregate` interface that produces a `MemoryStoredEvent`.
+
+```java
+@Tool(description = "Store a learned fact as a memory entry for the agentic aggregate")
+default MemoryStoredEvent storeMemory(
+ @ToolParam(description = "The unique identifier of the agentic aggregate instance") String agenticAggregateId,
+ @ToolParam(description = "A short 1-3 word topic label for the memory") String subject,
+ @ToolParam(description = "A clear, concise factual statement (max ~200 characters)") String fact,
+ @ToolParam(description = "Source reference - the command/event type that triggered this learning") String citations,
+ @ToolParam(description = "Why this fact is worth remembering - 2-3 sentences explaining significance") String reason) { ... }
```
-**Behavior**:
-1. Reads the current aggregate ID from the blackboard (set by the partition before agent invocation)
-2. Takes `subject`, `fact`, `citations`, and `reason` as inputs (from blackboard or method parameters)
-3. Creates a `MemoryStoredEvent` and returns it on the blackboard
-4. The partition collects the event and applies it through the built-in `@EventSourcingHandler` (which calls `MemoryAwareState.withMemory()`)
+**Behavior**: Creates and returns a `MemoryStoredEvent` with a random UUID and current timestamp. The event is collected from the `MemoryLearningResult` by `AgentProcessResultTranslator` and applied through the built-in `@EventSourcingHandler`.
-**Why**: This is the most fundamental agent action — the ability to learn and persist knowledge across interactions. Producing events directly (rather than commands) simplifies the flow and is consistent with the event-sourcing model where actions result in domain events.
+**Why**: By placing the method on the `AgenticAggregate` interface and annotating it with `@Tool`, the LLM can call it as a function tool during the `createObject()` call in `learnFromProcess`. This eliminates the need for a separate Embabel `@Action` and leverages Spring AI's tool-calling mechanism.
-### 2.3 Memory Management Action: `ForgetMemoryAction`
+### ~~2.3 Memory Management Action: `ForgetMemoryAction`~~ — MOVED TO `AgenticAggregate` INTERFACE
-**What**: An `@Action`-annotated method that produces a `MemoryRevokedEvent` to remove a specific memory by ID or by criteria.
+> **Note**: The `forgetMemory` method has been moved from `AkcesAgentComponent` (as an `@Action`) to the `AgenticAggregate` interface (as a `@Tool`-annotated default method).
-```
-@Action(description = "Revoke a memory entry that is no longer relevant or accurate")
+**What**: A `@Tool`-annotated default method on the `AgenticAggregate` interface that produces a `MemoryRevokedEvent`.
+
+```java
+@Tool(description = "Revoke a memory entry that is no longer relevant or accurate")
+default MemoryRevokedEvent forgetMemory(
+ @ToolParam(description = "The unique identifier of the agentic aggregate instance") String agenticAggregateId,
+ @ToolParam(description = "The UUID of the memory entry to revoke") String memoryId,
+ @ToolParam(description = "The reason the memory is being revoked") String reason) { ... }
```
-**Behavior**:
-1. Takes `memoryId` and `reason` from the blackboard
-2. Creates a `MemoryRevokedEvent` and returns it on the blackboard
-3. The partition collects the event and applies it through the built-in `@EventSourcingHandler` (which calls `MemoryAwareState.withoutMemory()`)
-4. Enables the agent to self-manage its knowledge base
+**Behavior**: Creates and returns a `MemoryRevokedEvent`. The event is collected from the `MemoryLearningResult` by `AgentProcessResultTranslator`.
+
+**Why**: Same rationale as `storeMemory` — placing the method on the aggregate interface allows the LLM to call it as a tool during the learning process.
-**Why**: Agents must be able to correct their knowledge. Outdated or incorrect facts should be removable.
+**Dependency changes**: `MemoryStoredEvent` and `MemoryRevokedEvent` have been moved from `main/agentic` to `main/api` (package `org.elasticsoftware.akces.agentic.events`) since they only depend on API-module classes and are now referenced by the `AgenticAggregate` interface. The `spring-ai-model` dependency is added to the `api` module as `
+ *
+ *
+ *
+ *
+ *
+ * @param memories the current list of memories from the blackboard
+ * @return a {@link MemoryLearningResult} summarizing what was learned
+ */
+ @AchievesGoal(description = """
+ Analyze current session information and distill useful memories.
+
+ Constraints:
+ - Maximum 3 new memories per agent process execution (prevents memory bloat)
+ - No duplicate memories: check existing memories before storing and skip any \
+ fact that is already stored (same subject + substantially similar fact content)
+ - Memory capacity enforcement: after storing new memories, if total memory count \
+ exceeds maxMemories, evict oldest entries using ForgetMemoryAction until within limit
+
+ Memory field guidance:
+ - subject: 1-3 word topic label (e.g. "error handling", "user preferences")
+ - fact: clear, concise factual statement (max ~200 chars), actionable and self-contained
+ - citations: source reference — the command/event type that triggered this learning
+ - reason: why this fact is worth remembering, 2-3 sentences explaining significance
+
+ Plan:
+ 1. RecallMemoriesAction to check existing memories (avoid duplicates, assess capacity)
+ 2. Determine what new facts were learned from the session context (max 3)
+ 3. StoreMemoryAction for each new memory
+ 4. ForgetMemoryAction for oldest memories if capacity exceeded""")
+ @Action(description = """
+ Complete the learning process by analyzing the current agent process session \
+ context and distilling useful memories. This action produces a summary of \
+ the memory management operations performed. Maximum 3 new memories per execution. \
+ No duplicates. Evict oldest if capacity exceeded.""")
+ public MemoryLearningResult learnFromProcess(List
+ *
+ */
+class AkcesAgentComponentTest {
+
+ private AkcesAgentComponent component;
+
+ @BeforeEach
+ void setUp() {
+ component = new AkcesAgentComponent();
+ }
+
+ // =========================================================================
+ // Annotation verification
+ // =========================================================================
+
+ @Nested
+ class AnnotationTests {
+
+ @Test
+ void classShouldBeAnnotatedWithEmbabelComponent() {
+ assertThat(AkcesAgentComponent.class.isAnnotationPresent(EmbabelComponent.class))
+ .as("AkcesAgentComponent must be annotated with @EmbabelComponent")
+ .isTrue();
+ }
+
+ @Test
+ void storeMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
+ Method method = AkcesAgentComponent.class.getMethod(
+ "storeMemory", String.class, String.class, String.class,
+ String.class, String.class);
+ Action action = method.getAnnotation(Action.class);
+
+ assertThat(action).isNotNull();
+ assertThat(action.description())
+ .isEqualTo("Store a learned fact as a memory entry for the agentic aggregate");
+ assertThat(action.readOnly()).isFalse();
+ }
+
+ @Test
+ void forgetMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
+ Method method = AkcesAgentComponent.class.getMethod(
+ "forgetMemory", String.class, String.class, String.class);
+ Action action = method.getAnnotation(Action.class);
+
+ assertThat(action).isNotNull();
+ assertThat(action.description())
+ .isEqualTo("Revoke a memory entry that is no longer relevant or accurate");
+ assertThat(action.readOnly()).isFalse();
+ }
+
+ @Test
+ void recallMemoriesShouldBeAnnotatedWithReadOnlyAction() throws NoSuchMethodException {
+ Method method = AkcesAgentComponent.class.getMethod(
+ "recallMemories", List.class, String.class);
+ Action action = method.getAnnotation(Action.class);
+
+ assertThat(action).isNotNull();
+ assertThat(action.description()).contains("Search stored memories");
+ assertThat(action.readOnly())
+ .as("recallMemories must be readOnly")
+ .isTrue();
+ }
+
+ @Test
+ void hasMemoriesShouldBeAnnotatedWithCondition() throws NoSuchMethodException {
+ Method method = AkcesAgentComponent.class.getMethod("hasMemories", List.class);
+ Condition condition = method.getAnnotation(Condition.class);
+
+ assertThat(condition).isNotNull();
+ assertThat(condition.name()).isEqualTo("hasMemories");
+ }
+
+ @Test
+ void learnFromProcessShouldBeAnnotatedWithAchievesGoalAndAction()
+ throws NoSuchMethodException {
+ Method method = AkcesAgentComponent.class.getMethod("learnFromProcess", List.class);
+
+ AchievesGoal achievesGoal = method.getAnnotation(AchievesGoal.class);
+ assertThat(achievesGoal).isNotNull();
+ assertThat(achievesGoal.description())
+ .contains("Analyze current session information")
+ .contains("Maximum 3 new memories")
+ .contains("No duplicate memories")
+ .contains("Memory capacity enforcement");
+
+ Action action = method.getAnnotation(Action.class);
+ assertThat(action).isNotNull();
+ assertThat(action.description()).contains("learning process");
+ }
+ }
+
+ // =========================================================================
+ // StoreMemoryAction tests
+ // =========================================================================
+
+ @Nested
+ class StoreMemoryActionTests {
+
+ @Test
+ void shouldProduceMemoryStoredEventWithCorrectFields() {
+ MemoryStoredEvent event = component.storeMemory(
+ "agg-1", "testing", "Use JUnit 5",
+ "build.gradle:10", "consistency");
+
+ assertThat(event.agenticAggregateId()).isEqualTo("agg-1");
+ assertThat(event.subject()).isEqualTo("testing");
+ assertThat(event.fact()).isEqualTo("Use JUnit 5");
+ assertThat(event.citations()).isEqualTo("build.gradle:10");
+ assertThat(event.reason()).isEqualTo("consistency");
+ assertThat(event.memoryId()).isNotNull().isNotBlank();
+ assertThat(event.storedAt()).isNotNull();
+ }
+
+ @Test
+ void shouldGenerateUniqueMemoryIds() {
+ MemoryStoredEvent event1 = component.storeMemory(
+ "agg-1", "s", "f", "c", "r");
+ MemoryStoredEvent event2 = component.storeMemory(
+ "agg-1", "s", "f", "c", "r");
+
+ assertThat(event1.memoryId()).isNotEqualTo(event2.memoryId());
+ }
+
+ @Test
+ void shouldSetStoredAtToCurrentTime() {
+ Instant before = Instant.now();
+ MemoryStoredEvent event = component.storeMemory(
+ "agg-1", "s", "f", "c", "r");
+ Instant after = Instant.now();
+
+ assertThat(event.storedAt())
+ .isAfterOrEqualTo(before)
+ .isBeforeOrEqualTo(after);
+ }
+
+ @Test
+ void shouldSetCorrectAggregateId() {
+ MemoryStoredEvent event = component.storeMemory(
+ "agg-1", "s", "f", "c", "r");
+
+ assertThat(event.getAggregateId()).isEqualTo("agg-1");
+ }
+ }
+
+ // =========================================================================
+ // ForgetMemoryAction tests
+ // =========================================================================
+
+ @Nested
+ class ForgetMemoryActionTests {
+
+ @Test
+ void shouldProduceMemoryRevokedEventWithCorrectFields() {
+ MemoryRevokedEvent event = component.forgetMemory(
+ "agg-1", "mem-42", "no longer relevant");
+
+ assertThat(event.agenticAggregateId()).isEqualTo("agg-1");
+ assertThat(event.memoryId()).isEqualTo("mem-42");
+ assertThat(event.reason()).isEqualTo("no longer relevant");
+ assertThat(event.revokedAt()).isNotNull();
+ }
+
+ @Test
+ void shouldSetRevokedAtToCurrentTime() {
+ Instant before = Instant.now();
+ MemoryRevokedEvent event = component.forgetMemory(
+ "agg-1", "mem-1", "eviction");
+ Instant after = Instant.now();
+
+ assertThat(event.revokedAt())
+ .isAfterOrEqualTo(before)
+ .isBeforeOrEqualTo(after);
+ }
+
+ @Test
+ void shouldSetCorrectAggregateId() {
+ MemoryRevokedEvent event = component.forgetMemory(
+ "agg-1", "mem-1", "reason");
+
+ assertThat(event.getAggregateId()).isEqualTo("agg-1");
+ }
+ }
+
+ // =========================================================================
+ // RecallMemoriesAction tests
+ // =========================================================================
+
+ @Nested
+ class RecallMemoriesActionTests {
+
+ private final ListGoals
*
*
*
* @see MemoryStoredEvent
@@ -174,9 +176,8 @@ public List
*
*
- *
*
*
* @param memories the current list of memories from the blackboard
+ * @param context the Embabel {@link OperationContext} providing access to LLM
+ * capabilities via {@link OperationContext#ai()}
* @return a {@link MemoryLearningResult} summarizing what was learned
*/
- @AchievesGoal(description = """
- Analyze current session information and distill useful memories.
-
- Constraints:
- - Maximum 3 new memories per agent process execution (prevents memory bloat)
- - No duplicate memories: check existing memories before storing and skip any
- fact that is already stored (same subject + substantially similar fact content)
- - Memory capacity enforcement: after storing new memories, if total memory count
- exceeds maxMemories, evict oldest entries using ForgetMemoryAction until within limit
-
- Memory field guidance:
- - subject: 1-3 word topic label (e.g. "error handling", "user preferences")
- - fact: clear, concise factual statement (max ~200 chars), actionable and self-contained
- - citations: source reference — the command/event type that triggered this learning
- - reason: why this fact is worth remembering, 2-3 sentences explaining significance
-
- Plan:
- 1. RecallMemoriesAction to check existing memories (avoid duplicates, assess capacity)
- 2. Determine what new facts were learned from the session context (max 3)
- 3. StoreMemoryAction for each new memory
- 4. ForgetMemoryAction for oldest memories if capacity exceeded""")
- @Action(description = """
- Complete the learning process by analyzing the current agent process session
- context and distilling useful memories. This action produces a summary of
- the memory management operations performed. Maximum 3 new memories per execution.
- No duplicates. Evict oldest if capacity exceeded.""")
- public MemoryLearningResult learnFromProcess(ListActions
*
- *
@@ -59,11 +56,11 @@
*
*
*
- * @see MemoryStoredEvent
- * @see MemoryRevokedEvent
+ * @see AgenticAggregate#storeMemory
+ * @see AgenticAggregate#forgetMemory
* @see AgenticAggregateMemory
* @see MemoryLearningResult
*/
@@ -80,74 +77,6 @@ public class AkcesAgentComponent {
// Actions
// -------------------------------------------------------------------------
- /**
- * Stores a learned fact as a new memory entry for the agentic aggregate.
- *
- *
- *
+ *
+ *
*
*
@@ -235,24 +160,28 @@ public boolean hasMemories(List
*
- * @param memories the current list of memories from the blackboard
- * @param context the Embabel {@link OperationContext} providing access to LLM
- * capabilities via {@link OperationContext#ai()}
- * @return a {@link MemoryLearningResult} summarizing what was learned
+ * @param memories the current list of memories from the blackboard
+ * @param aggregate the {@link AgenticAggregate} instance, passed as a tool object
+ * to expose {@code storeMemory()} and {@code forgetMemory()} to the LLM
+ * @param context the Embabel {@link OperationContext} providing access to LLM
+ * capabilities via {@link OperationContext#ai()}
+ * @return a {@link MemoryLearningResult} containing the events produced and a summary
*/
@AchievesGoal(description = "Learned from current session and stored and/or removed memories")
@Action(description = "Analyze current session and distill useful memories using LLM reasoning")
public MemoryLearningResult learnFromProcess(
List
*
- *
*/
@@ -82,21 +79,6 @@ void classShouldBeAnnotatedWithEmbabelComponent() {
.isTrue();
}
- @Test
- void storeMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
- var method = AkcesAgentComponent.class.getMethod(
- "storeMemory", String.class, String.class, String.class,
- String.class, String.class);
- assertThat(method.isAnnotationPresent(Action.class)).isTrue();
- }
-
- @Test
- void forgetMemoryShouldBeAnnotatedWithAction() throws NoSuchMethodException {
- var method = AkcesAgentComponent.class.getMethod(
- "forgetMemory", String.class, String.class, String.class);
- assertThat(method.isAnnotationPresent(Action.class)).isTrue();
- }
-
@Test
void recallMemoriesShouldBeAnnotatedWithReadOnlyAction() throws NoSuchMethodException {
var method = AkcesAgentComponent.class.getMethod(
@@ -116,104 +98,13 @@ void hasMemoriesShouldBeAnnotatedWithCondition() throws NoSuchMethodException {
void learnFromProcessShouldBeAnnotatedWithAchievesGoalAndAction()
throws NoSuchMethodException {
var method = AkcesAgentComponent.class.getMethod(
- "learnFromProcess", List.class, OperationContext.class);
+ "learnFromProcess", List.class, AgenticAggregate.class,
+ OperationContext.class);
assertThat(method.isAnnotationPresent(AchievesGoal.class)).isTrue();
assertThat(method.isAnnotationPresent(Action.class)).isTrue();
}
}
- // =========================================================================
- // StoreMemoryAction tests
- // =========================================================================
-
- @Nested
- class StoreMemoryActionTests {
-
- @Test
- void shouldProduceMemoryStoredEventWithCorrectFields() {
- MemoryStoredEvent event = component.storeMemory(
- "agg-1", "testing", "Use JUnit 5",
- "build.gradle:10", "consistency");
-
- assertThat(event.agenticAggregateId()).isEqualTo("agg-1");
- assertThat(event.subject()).isEqualTo("testing");
- assertThat(event.fact()).isEqualTo("Use JUnit 5");
- assertThat(event.citations()).isEqualTo("build.gradle:10");
- assertThat(event.reason()).isEqualTo("consistency");
- assertThat(event.memoryId()).isNotNull().isNotBlank();
- assertThat(event.storedAt()).isNotNull();
- }
-
- @Test
- void shouldGenerateUniqueMemoryIds() {
- MemoryStoredEvent event1 = component.storeMemory(
- "agg-1", "s", "f", "c", "r");
- MemoryStoredEvent event2 = component.storeMemory(
- "agg-1", "s", "f", "c", "r");
-
- assertThat(event1.memoryId()).isNotEqualTo(event2.memoryId());
- }
-
- @Test
- void shouldSetStoredAtToCurrentTime() {
- Instant before = Instant.now();
- MemoryStoredEvent event = component.storeMemory(
- "agg-1", "s", "f", "c", "r");
- Instant after = Instant.now();
-
- assertThat(event.storedAt())
- .isAfterOrEqualTo(before)
- .isBeforeOrEqualTo(after);
- }
-
- @Test
- void shouldSetCorrectAggregateId() {
- MemoryStoredEvent event = component.storeMemory(
- "agg-1", "s", "f", "c", "r");
-
- assertThat(event.getAggregateId()).isEqualTo("agg-1");
- }
- }
-
- // =========================================================================
- // ForgetMemoryAction tests
- // =========================================================================
-
- @Nested
- class ForgetMemoryActionTests {
-
- @Test
- void shouldProduceMemoryRevokedEventWithCorrectFields() {
- MemoryRevokedEvent event = component.forgetMemory(
- "agg-1", "mem-42", "no longer relevant");
-
- assertThat(event.agenticAggregateId()).isEqualTo("agg-1");
- assertThat(event.memoryId()).isEqualTo("mem-42");
- assertThat(event.reason()).isEqualTo("no longer relevant");
- assertThat(event.revokedAt()).isNotNull();
- }
-
- @Test
- void shouldSetRevokedAtToCurrentTime() {
- Instant before = Instant.now();
- MemoryRevokedEvent event = component.forgetMemory(
- "agg-1", "mem-1", "eviction");
- Instant after = Instant.now();
-
- assertThat(event.revokedAt())
- .isAfterOrEqualTo(before)
- .isBeforeOrEqualTo(after);
- }
-
- @Test
- void shouldSetCorrectAggregateId() {
- MemoryRevokedEvent event = component.forgetMemory(
- "agg-1", "mem-1", "reason");
-
- assertThat(event.getAggregateId()).isEqualTo("agg-1");
- }
- }
-
// =========================================================================
// RecallMemoriesAction tests
// =========================================================================
@@ -372,40 +263,52 @@ void shouldReturnTrueWhenMultipleMemoriesExist() {
class LearnFromProcessGoalTests {
@Test
- void shouldInvokeLlmViaOperationContextAndReturnResult() {
- var expectedResult = new MemoryLearningResult(2, 1, "Stored 2, revoked 1");
+ void shouldInvokeLlmWithToolObjectAndReturnResult() {
+ var storedEvents = List.of(
+ new MemoryStoredEvent("agg-1", "m1", "topic", "fact", "cite", "why", Instant.now()));
+ var revokedEvents = List.
+ *
+ *
+ * the aggregate state type
+ * @see MemoryStoredEvent
+ * @see MemoryRevokedEvent
+ * @see AgenticAggregateMemory
+ */
public interface AgenticAggregate extends Aggregate {
/**
* Returns the memories held by the given state.
@@ -36,4 +65,70 @@ default List