Skip to content

feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions)#318

Closed
Copilot wants to merge 7 commits intomainfrom
copilot/embabel-phase-2-goals-actions-conditions
Closed

feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions)#318
Copilot wants to merge 7 commits intomainfrom
copilot/embabel-phase-2-goals-actions-conditions

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 9, 2026

Adds AkcesAgentComponent, the @EmbabelComponent that provides reusable memory management capabilities for all agentic aggregates. Memory storage and revocation are exposed as @Tool-annotated default methods on the AgenticAggregate interface, callable by the LLM during the learning goal. Other actions and conditions are discoverable by AgentPlatform via component scanning.

AgenticAggregate interface — @Tool methods (api module)

  • storeMemory()@Tool (Spring AI) default method → MemoryStoredEvent (UUID + timestamp generated internally)
  • forgetMemory()@Tool (Spring AI) default method → MemoryRevokedEvent

These 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

  • MemoryStoredEvent and MemoryRevokedEvent moved from main/agentic to main/api (package org.elasticsoftware.akces.agentic.events) since they are now referenced by AgenticAggregate
  • spring-ai-model added as an <optional> dependency to the api module for @Tool and @ToolParam annotations

New classes in org.elasticsoftware.akces.agentic.agent

  • AkcesAgentComponent@EmbabelComponent housing actions, conditions, and goals:

    • recallMemories()@Action(readOnly=true) — locale-independent case-insensitive filtering across subject/fact/reason fields using String.regionMatches
    • hasMemories()@Condition(name="hasMemories") — precondition for memory-dependent actions
    • learnFromProcess()@AchievesGoal + @Action — accepts AgenticAggregate<?> and OperationContext, uses context.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 containing List<MemoryStoredEvent>, List<MemoryRevokedEvent>, and a String summary

Goal design

LearnFromProcessGoal is a Goal (not a simple Action) because memory distillation requires LLM reasoning with tool use. The @AchievesGoal description declares the outcome, while constraint details are passed to the LLM via the prompt. The AgenticAggregate instance is passed as a tool object so the LLM can call storeMemory() and forgetMemory():

@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<AgenticAggregateMemory> memories,
        AgenticAggregate<?> aggregate,
        OperationContext context) {
    // ... builds prompt with constraints and memory field guidance ...
    return context.ai()
            .withDefaultLlm()
            .withToolObject(aggregate)
            .createObject(prompt, MemoryLearningResult.class);
}

Handler adapter changes

  • AgenticCommandHandlerFunctionAdapter and AgenticEventHandlerFunctionAdapter now put the AgenticAggregate instance on the blackboard as "aggregate" for type-matched injection into the learning goal
  • AgentProcessResultTranslator extracts events from MemoryLearningResult objects on the blackboard (in addition to direct DomainEvent objects)

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.

Copilot AI and others added 2 commits April 9, 2026 05:58
… 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>
Copilot AI changed the title [WIP] Add Akces Embabel component with goals, actions, and conditions feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions) Apr 9, 2026
Copilot AI requested a review from jwijgerd April 9, 2026 06:02
@jwijgerd jwijgerd marked this pull request as ready for review April 9, 2026 10:07
Copilot AI review requested due to automatic review settings April 9, 2026 10:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 a hasMemories Condition.
  • Adds MemoryLearningResult record as the goal/action output type for learnFromProcess.
  • Adds AkcesAgentComponentTest covering 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.

Comment on lines +174 to +180
if (query == null || query.isBlank()) {
return List.copyOf(memories);
}
String lowerQuery = query.toLowerCase();
return memories.stream()
.filter(m -> matchesQuery(m, lowerQuery))
.toList();
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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, ...)).

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +298
* @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);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
* @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;

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +25
* 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
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
* 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

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +267
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);
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +142
@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");
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@jwijgerd jwijgerd left a comment

Choose a reason for hiding this comment

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

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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

check the other Action method and see if we also need to add the operation context

Checked all other Action methods — storeMemory, forgetMemory, and recallMemories are deterministic operations (produce events or filter data) that don't require LLM reasoning, so they don't need OperationContext. Only learnFromProcess needs it for the context.ai() call.

Copilot AI requested a review from jwijgerd April 9, 2026 10:47
Copy link
Copy Markdown
Contributor

@jwijgerd jwijgerd left a comment

Choose a reason for hiding this comment

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

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

Copilot AI and others added 2 commits April 9, 2026 16:03
…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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

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 p...

Done in dc37ad2. Changes:

  1. storeMemory and forgetMemory are now @Tool-annotated (Spring AI org.springframework.ai.tool.annotation.Tool) default methods on AgenticAggregate interface, with @ToolParam descriptions on each parameter.

  2. MemoryLearningResult now has List<MemoryStoredEvent> and List<MemoryRevokedEvent> instead of int counters.

  3. learnFromProcess accepts AgenticAggregate<?> and uses context.ai().withDefaultLlm().withToolObject(aggregate).createObject(prompt, MemoryLearningResult.class) — the LLM calls the @Tool methods during reasoning.

  4. Handler adapters now put the AgenticAggregate instance on the blackboard as "aggregate".

  5. AgentProcessResultTranslator extracts events from MemoryLearningResult objects on the blackboard.

  6. MemoryStoredEvent/MemoryRevokedEvent moved to api module since they're now referenced by AgenticAggregate. spring-ai-model added as optional dependency to api module.

  7. Plan sections 2.2, 2.3, 2.7 updated accordingly.

Copilot AI requested a review from jwijgerd April 9, 2026 17:06
@jwijgerd
Copy link
Copy Markdown
Contributor

will change the implementation plan

@jwijgerd jwijgerd closed this Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(embabel): Phase 2 — Akces Embabel Component (Goals, Actions, Conditions)

3 participants