feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions)#318
feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions)#318
Conversation
… Conditions 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>
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>
There was a problem hiding this comment.
Pull request overview
Adds an Embabel-discoverable agent component to the agentic module, centralizing reusable memory-management actions/conditions and a learning goal output type to support agentic aggregates.
Changes:
- Introduces
AkcesAgentComponent(@EmbabelComponent) with Actions for storing/forgetting/recalling memories and ahasMemoriesCondition. - Adds
MemoryLearningResultrecord as the goal/action output type forlearnFromProcess. - Adds
AkcesAgentComponentTestcovering action outputs, filtering behavior, and annotation attributes.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponent.java |
New Embabel component providing memory-related actions/condition and a learning goal action. |
main/agentic/src/main/java/org/elasticsoftware/akces/agentic/agent/MemoryLearningResult.java |
New record defining the learning goal/action result payload. |
main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java |
New unit tests for memory actions/filtering plus annotation assertions. |
| if (query == null || query.isBlank()) { | ||
| return List.copyOf(memories); | ||
| } | ||
| String lowerQuery = query.toLowerCase(); | ||
| return memories.stream() | ||
| .filter(m -> matchesQuery(m, lowerQuery)) | ||
| .toList(); |
There was a problem hiding this comment.
recallMemories lowercases the query using String#toLowerCase() without an explicit locale. This makes the case-insensitive search behave differently under certain default locales (e.g., Turkish), which can lead to missed matches. Use toLowerCase(Locale.ROOT) (and apply the same locale handling in the text normalization) or switch to a locale-independent case-insensitive match (e.g., regionMatches(true, ...)).
| * @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); |
There was a problem hiding this comment.
containsIgnoreCase uses text.toLowerCase().contains(lower) which is both locale-sensitive and allocates a new lowercased string per check. Prefer a locale-stable approach (e.g., toLowerCase(Locale.ROOT) once) or a case-insensitive search that avoids full-string allocations.
| * @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); | |
| * @param lower the search term | |
| * @return {@code true} if {@code text} contains {@code lower} (case-insensitive) | |
| */ | |
| private static boolean containsIgnoreCase(String text, String lower) { | |
| if (text == null) { | |
| return false; | |
| } | |
| int searchLength = lower.length(); | |
| int maxStart = text.length() - searchLength; | |
| for (int i = 0; i <= maxStart; i++) { | |
| if (text.regionMatches(true, i, lower, 0, searchLength)) { | |
| return true; | |
| } | |
| } | |
| return false; |
| * Result object produced by the {@code LearnFromProcessGoal} when the learning | ||
| * cycle completes. | ||
| * | ||
| * <p>This record is the output type of the | ||
| * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} goal action and serves |
There was a problem hiding this comment.
The Javadoc refers to LearnFromProcessGoal, but there is no such type in the module; the implemented entry point is AkcesAgentComponent#learnFromProcess. Update the wording to reference the actual goal/action name to avoid misleading documentation and broken references as the code evolves.
| * Result object produced by the {@code LearnFromProcessGoal} when the learning | |
| * cycle completes. | |
| * | |
| * <p>This record is the output type of the | |
| * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} goal action and serves | |
| * Result object produced by the {@link AkcesAgentComponent#learnFromProcess | |
| * learnFromProcess} action when the learning cycle completes. | |
| * | |
| * <p>This record is the output type of the | |
| * {@link AkcesAgentComponent#learnFromProcess learnFromProcess} action and serves |
| 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<AgenticAggregateMemory> memories) { | ||
| int currentCount = (memories != null) ? memories.size() : 0; | ||
| return new MemoryLearningResult(0, 0, | ||
| "Learning process completed. Current memory count: " + currentCount); |
There was a problem hiding this comment.
learnFromProcess’s @Action description and return type imply it summarizes memory operations performed (stores/revocations, capacity enforcement), but the implementation always returns memoriesStored=0 and memoriesRevoked=0 and doesn’t perform or observe any operations. Either implement the described behavior (or accept required inputs from the blackboard to compute counts), or adjust the action/record docs so the returned counts/summary can’t be interpreted as reflecting real memory changes.
| 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<AgenticAggregateMemory> memories) { | |
| int currentCount = (memories != null) ? memories.size() : 0; | |
| return new MemoryLearningResult(0, 0, | |
| "Learning process completed. Current memory count: " + currentCount); | |
| Complete the learning process by reporting a lightweight status summary for | |
| the current memory set available to this component. This action does not | |
| itself perform or verify memory storage or revocation operations; the returned | |
| counts are therefore not derived from observed memory mutations.""") | |
| /** | |
| * Returns a lightweight completion summary for the current learning step. | |
| * | |
| * <p>This method only inspects the provided memory list and reports the current | |
| * memory count in the summary text. It does not perform memory writes, revoke | |
| * memories, or observe whether other actions in the overall plan have done so. | |
| * Accordingly, the numeric fields in the returned {@link MemoryLearningResult} | |
| * are not interpreted here as counts of actual memory changes. | |
| * | |
| * @param memories the currently available memories; may be {@code null} | |
| * @return a status summary for the current memory set | |
| */ | |
| public MemoryLearningResult learnFromProcess(List<AgenticAggregateMemory> memories) { | |
| int currentCount = (memories != null) ? memories.size() : 0; | |
| return new MemoryLearningResult(0, 0, | |
| "Learning process status recorded. Current memory count: " + currentCount); |
| @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"); | ||
| } |
There was a problem hiding this comment.
The AnnotationTests nested suite is directly asserting annotation presence/attribute strings via reflection. This is brittle (fails on harmless wording changes) and conflicts with the project testing guidance to avoid dedicated tests for annotations; prefer testing the observable behavior (e.g., that actions produce the expected events / filtering works) or a higher-level discovery/integration test if annotation-driven discovery must be validated.
jwijgerd
left a comment
There was a problem hiding this comment.
check the other Action method and see if we also need to add the operation context
| 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<AgenticAggregateMemory> memories) { |
There was a problem hiding this comment.
You should inject an OperationContext (from the Embabel API) here and then use it to run an LLM using context.ai(). what's currently in the AchievesGoal desctiption should be in the prompt object. See an example here: https://docs.embabel.com/embabel-agent/guide/0.3.5/#example-writeandreviewagent. The AchievesGoal description should be Learned from current session and stored and/or removed memories
There was a problem hiding this comment.
Done in 57f1531. learnFromProcess now accepts OperationContext and uses context.ai().withDefaultLlm().createObject(prompt, MemoryLearningResult.class) for LLM reasoning. The @AchievesGoal description is updated to "Learned from current session and stored and/or removed memories" and the constraint details are moved into the prompt string passed to the LLM.
…ing, clean up tests - learnFromProcess now accepts OperationContext and uses context.ai() .withDefaultLlm().createObject() for LLM-powered memory distillation - @AchievesGoal description updated to "Learned from current session and stored and/or removed memories"; constraint details moved to prompt - containsIgnoreCase now uses String.regionMatches(true, ...) for locale-independent case-insensitive matching - MemoryLearningResult Javadoc fixed to reference AkcesAgentComponent - Annotation tests simplified to check presence only, not description strings - LearnFromProcess tests now mock OperationContext/Ai/PromptRunner Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/d244d898-589d-4819-b976-ed409e8ef8a6 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
main/agentic/src/test/java/org/elasticsoftware/akces/agentic/agent/AkcesAgentComponentTest.java
Fixed
Show fixed
Hide fixed
Checked all other Action methods — |
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/d244d898-589d-4819-b976-ed409e8ef8a6 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
jwijgerd
left a comment
There was a problem hiding this comment.
I want you to change the following: the storeMemory and the forgetMemory actions should be @tool annotated (default) methods on the AgenticAggregateInterface. The AgenticAggregate instance should be passed to the withToolObject() method on the ai() instance in the agentic handlers. The MemoryLearningResult should have a List and a List instead of the counters
…gregate, update MemoryLearningResult - Move MemoryStoredEvent/MemoryRevokedEvent to api module (no agentic deps) - Add spring-ai-model optional dependency to api module for @tool annotation - Add @tool default methods storeMemory() and forgetMemory() on AgenticAggregate - Remove storeMemory/forgetMemory @action methods from AkcesAgentComponent - Update learnFromProcess to accept AgenticAggregate and pass via withToolObject() - Change MemoryLearningResult to use List<MemoryStoredEvent> and List<MemoryRevokedEvent> - Update AgentProcessResultTranslator to extract events from MemoryLearningResult - Add AgenticAggregate instance to handler adapter blackboard bindings - Update all tests 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>
- 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>
Done in
|
|
will change the implementation plan |
Adds
AkcesAgentComponent, the@EmbabelComponentthat provides reusable memory management capabilities for all agentic aggregates. Memory storage and revocation are exposed as@Tool-annotated default methods on theAgenticAggregateinterface, callable by the LLM during the learning goal. Other actions and conditions are discoverable byAgentPlatformvia component scanning.AgenticAggregateinterface —@Toolmethods (api module)storeMemory()—@Tool(Spring AI) default method →MemoryStoredEvent(UUID + timestamp generated internally)forgetMemory()—@Tool(Spring AI) default method →MemoryRevokedEventThese are exposed to the LLM via
PromptRunner.withToolObject(aggregate)during the learning goal, allowing the LLM to call them as function tools during memory distillation.Module changes
MemoryStoredEventandMemoryRevokedEventmoved frommain/agentictomain/api(packageorg.elasticsoftware.akces.agentic.events) since they are now referenced byAgenticAggregatespring-ai-modeladded as an<optional>dependency to theapimodule for@Tooland@ToolParamannotationsNew classes in
org.elasticsoftware.akces.agentic.agentAkcesAgentComponent—@EmbabelComponenthousing actions, conditions, and goals:recallMemories()—@Action(readOnly=true)— locale-independent case-insensitive filtering across subject/fact/reason fields usingString.regionMatcheshasMemories()—@Condition(name="hasMemories")— precondition for memory-dependent actionslearnFromProcess()—@AchievesGoal+@Action— acceptsAgenticAggregate<?>andOperationContext, usescontext.ai().withDefaultLlm().withToolObject(aggregate).createObject()for LLM-powered memory distillation (max 3 new memories, no duplicates, capacity enforcement via oldest eviction)MemoryLearningResult— output record for the learning goal containingList<MemoryStoredEvent>,List<MemoryRevokedEvent>, and aString summaryGoal design
LearnFromProcessGoalis a Goal (not a simple Action) because memory distillation requires LLM reasoning with tool use. The@AchievesGoaldescription declares the outcome, while constraint details are passed to the LLM via the prompt. TheAgenticAggregateinstance is passed as a tool object so the LLM can callstoreMemory()andforgetMemory():Handler adapter changes
AgenticCommandHandlerFunctionAdapterandAgenticEventHandlerFunctionAdapternow put theAgenticAggregateinstance on the blackboard as"aggregate"for type-matched injection into the learning goalAgentProcessResultTranslatorextracts events fromMemoryLearningResultobjects on the blackboard (in addition to directDomainEventobjects)Tests
23 new unit tests covering annotation presence verification, recall filtering edge cases, condition evaluation, and goal behavior with mocked
OperationContext/Ai/PromptRunner/AgenticAggregate. All 67 agentic tests and 32 api tests pass.