-
Notifications
You must be signed in to change notification settings - Fork 0
Add MemoryDistillerAgent for post-completion memory distillation #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jwijgerd
merged 11 commits into
main
from
copilot/distill-relevant-memories-completed-process
Apr 11, 2026
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
b0dc255
Add MemoryDistillerAgent and memory distillation on completed AgentPr…
Copilot 29b7045
Address code review: extract local variable, remove redundant null check
Copilot 41bb39f
Merge branch 'main' into copilot/distill-relevant-memories-completed-…
jwijgerd 6aa01aa
Address PR review feedback: rename params, use events directly, renam…
Copilot eb1c8c2
Apply suggestions from code review
jwijgerd 3103bbc
Address review: GOAP planner, ObjectMapper injection, remove explicit…
Copilot 5bcc8c8
Add null check for ObjectMapper in MemoryDistillerAgent constructor
Copilot 3d6dda0
Use JSON markdown code fencing in prompt, change defaults to maxTotal…
Copilot c8ec319
Fix test failures and add agentTask to distiller blackboard bindings
Copilot 6358a91
Refactor: use strongly-typed MemoryDistillationInput for action metho…
Copilot ee51f3f
Remove unused aggregateId param from translateDistillationResult
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
...ntic/src/main/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillationInput.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /* | ||
| * 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.embabel; | ||
|
|
||
| import com.embabel.agent.core.ActionInvocation; | ||
| import org.elasticsoftware.akces.aggregate.AgenticAggregateMemory; | ||
| import org.elasticsoftware.akces.aggregate.AssignedTask; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Strongly-typed input for the {@link MemoryDistillerAgent} action method. | ||
| * | ||
| * <p>This record is placed on the Embabel Blackboard by the Akces runtime | ||
| * and injected into the agent's action method by the Embabel framework | ||
| * via type-based parameter resolution. | ||
| * | ||
| * @param agentTask the assigned task that triggered the completed process | ||
| * @param history the execution history (action invocations) from the completed process | ||
| * @param blackboardObjects all objects from the completed process's blackboard | ||
| * @param existingMemories the current list of memories from the aggregate state | ||
| * @param maxTotalMemories the total memory capacity of the aggregate | ||
| * @param maxMemoriesAdded the per-distillation budget for net new memories | ||
| */ | ||
| public record MemoryDistillationInput( | ||
| AssignedTask agentTask, | ||
| List<ActionInvocation> history, | ||
| List<Object> blackboardObjects, | ||
| List<AgenticAggregateMemory> existingMemories, | ||
| int maxTotalMemories, | ||
| int maxMemoriesAdded | ||
| ) { | ||
| } |
42 changes: 42 additions & 0 deletions
42
...tic/src/main/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillationResult.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| /* | ||
| * 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.embabel; | ||
|
|
||
| import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent; | ||
| import org.elasticsoftware.akces.agentic.events.MemoryStoredEvent; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Result object produced by the {@link MemoryDistillerAgent} after distilling | ||
| * relevant memories from a completed {@link com.embabel.agent.core.AgentProcess}. | ||
| * | ||
| * <p>Contains two lists: | ||
| * <ul> | ||
| * <li>{@code stored} — new memory entries as {@link MemoryStoredEvent}s</li> | ||
| * <li>{@code revoked} — memory entries to remove as {@link MemoryRevokedEvent}s</li> | ||
| * </ul> | ||
| * | ||
| * @param stored the list of memories to store | ||
| * @param revoked the list of memories to revoke | ||
| */ | ||
| public record MemoryDistillationResult( | ||
| List<MemoryStoredEvent> stored, | ||
| List<MemoryRevokedEvent> revoked | ||
| ) { | ||
| } |
197 changes: 197 additions & 0 deletions
197
...agentic/src/main/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillerAgent.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| /* | ||
| * 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.embabel; | ||
|
|
||
| import com.embabel.agent.api.annotation.AchievesGoal; | ||
| import com.embabel.agent.api.annotation.Action; | ||
| import com.embabel.agent.api.annotation.Agent; | ||
| import com.embabel.agent.api.common.ActionContext; | ||
| import com.embabel.agent.api.common.PlannerType; | ||
| import tools.jackson.databind.ObjectMapper; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| /** | ||
| * Embabel agent responsible for distilling relevant memories from a successfully | ||
| * completed {@link com.embabel.agent.core.AgentProcess}. | ||
| * | ||
| * <p>When an agent process finishes with status {@code COMPLETED}, this agent is | ||
| * invoked with the process's execution history and all objects from its blackboard. | ||
| * It uses an LLM to analyze the process results and determine which memories should | ||
| * be stored and which existing memories should be revoked. | ||
| * | ||
| * <p>The agent produces a {@link MemoryDistillationResult} containing: | ||
| * <ul> | ||
| * <li>A list of new memories to store</li> | ||
| * <li>A list of existing memories to revoke</li> | ||
| * </ul> | ||
| * | ||
| * <p>The net number of new memories (stored minus revoked) is constrained by both | ||
| * the {@code maxTotalMemories} capacity and the per-distillation budget | ||
| * {@code maxMemoriesAdded}. | ||
| */ | ||
| @Agent(name = MemoryDistillerAgent.AGENT_NAME, | ||
| description = "Distills relevant memories from a completed agent process", | ||
| planner = PlannerType.GOAP) | ||
| public class MemoryDistillerAgent { | ||
|
|
||
| /** | ||
| * The well-known agent name used to locate this agent on the | ||
| * {@link com.embabel.agent.core.AgentPlatform}. | ||
| */ | ||
| public static final String AGENT_NAME = "MemoryDistiller"; | ||
|
|
||
| private final ObjectMapper objectMapper; | ||
|
|
||
| /** | ||
| * Creates a new {@code MemoryDistillerAgent}. | ||
| * | ||
| * @param objectMapper the Jackson {@link ObjectMapper} used for JSON serialization | ||
| * of blackboard objects and memories in the LLM prompt | ||
| */ | ||
| public MemoryDistillerAgent(ObjectMapper objectMapper) { | ||
| this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper must not be null"); | ||
| } | ||
|
|
||
| /** | ||
| * Distills memories from a completed agent process by analyzing the process | ||
| * history and blackboard contents using an LLM. | ||
| * | ||
| * <p>The Embabel framework injects the {@link MemoryDistillationInput} parameter | ||
| * from the Blackboard via type-based resolution. The input contains: | ||
| * <ul> | ||
| * <li>{@code agentTask} — the {@link org.elasticsoftware.akces.aggregate.AssignedTask} | ||
| * that triggered the completed process</li> | ||
| * <li>{@code history} — the execution history from the completed process</li> | ||
| * <li>{@code blackboardObjects} — all objects from the completed process's blackboard</li> | ||
| * <li>{@code existingMemories} — the current list of memories from the aggregate state</li> | ||
| * <li>{@code maxTotalMemories} — the total memory capacity of the aggregate</li> | ||
| * <li>{@code maxMemoriesAdded} — the per-distillation budget for net new memories</li> | ||
| * </ul> | ||
| * | ||
| * @param input the strongly-typed distillation input, injected by the Embabel framework | ||
| * @param context the action context providing access to AI capabilities | ||
| * @return a {@link MemoryDistillationResult} with memories to store and revoke | ||
| */ | ||
| @Action(description = "Distill relevant memories from a completed agent process") | ||
| @AchievesGoal(description = "Distill and manage memories from completed agent processes") | ||
| public MemoryDistillationResult distillMemories(MemoryDistillationInput input, ActionContext context) { | ||
| int currentCount = input.existingMemories().size(); | ||
| int capacityLeft = Math.max(0, input.maxTotalMemories() - currentCount); | ||
| int effectiveLimit = Math.min(capacityLeft, input.maxMemoriesAdded()); | ||
|
|
||
| String prompt = buildPrompt(input.agentTask(), input.history(), input.blackboardObjects(), | ||
| input.existingMemories(), effectiveLimit); | ||
|
|
||
| return context.ai() | ||
| .withDefaultLlm() | ||
| .createObject(prompt, MemoryDistillationResult.class); | ||
| } | ||
|
|
||
| private String buildPrompt(Object agentTask, | ||
| List<?> history, | ||
| List<?> blackboardObjects, | ||
| List<?> existingMemories, | ||
| int maxNewMemories) { | ||
| var sb = new StringBuilder(); | ||
|
|
||
| sb.append(""" | ||
| You are a memory distillation agent. Analyze the following completed agent process \ | ||
| and determine which facts should be stored as new memories and which existing \ | ||
| memories should be revoked (because they are outdated, incorrect, or superseded). | ||
|
|
||
| CONSTRAINTS: | ||
| """); | ||
|
|
||
| sb.append("- The net number of new memories (stored count minus revoked count) must be <= ") | ||
| .append(maxNewMemories).append("\n"); | ||
|
|
||
| sb.append(""" | ||
| - Only store memories that are actionable, likely to remain relevant, and \ | ||
| cannot always be inferred from limited context | ||
| - Only revoke memories that are clearly outdated, incorrect, or superseded \ | ||
| by new information | ||
|
|
||
| """); | ||
|
|
||
| sb.append("AGENT TASK:\n```json\n"); | ||
| if (agentTask != null) { | ||
| sb.append(serialize(agentTask)).append("\n"); | ||
| } else { | ||
| sb.append("null\n"); | ||
| } | ||
| sb.append("```\n"); | ||
|
|
||
| sb.append("\nPROCESS HISTORY (actions executed):\n```json\n"); | ||
| if (history != null && !history.isEmpty()) { | ||
| sb.append("[\n"); | ||
| for (int i = 0; i < history.size(); i++) { | ||
| sb.append(serialize(history.get(i))); | ||
| if (i < history.size() - 1) sb.append(","); | ||
| sb.append("\n"); | ||
| } | ||
| sb.append("]\n"); | ||
| } else { | ||
| sb.append("[]\n"); | ||
| } | ||
| sb.append("```\n"); | ||
|
|
||
| sb.append("\nBLACKBOARD OBJECTS (process context and results):\n```json\n"); | ||
| if (blackboardObjects != null && !blackboardObjects.isEmpty()) { | ||
| sb.append("[\n"); | ||
| for (int i = 0; i < blackboardObjects.size(); i++) { | ||
| sb.append(serialize(blackboardObjects.get(i))); | ||
| if (i < blackboardObjects.size() - 1) sb.append(","); | ||
| sb.append("\n"); | ||
| } | ||
| sb.append("]\n"); | ||
| } else { | ||
| sb.append("[]\n"); | ||
| } | ||
| sb.append("```\n"); | ||
|
|
||
| sb.append("\nEXISTING MEMORIES:\n```json\n"); | ||
| if (existingMemories != null && !existingMemories.isEmpty()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be a JSON list with markdown fencing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Existing memories now rendered as a JSON array with |
||
| sb.append("[\n"); | ||
| for (int i = 0; i < existingMemories.size(); i++) { | ||
| sb.append(serialize(existingMemories.get(i))); | ||
| if (i < existingMemories.size() - 1) sb.append(","); | ||
| sb.append("\n"); | ||
| } | ||
| sb.append("]\n"); | ||
| } else { | ||
| sb.append("[]\n"); | ||
| } | ||
| sb.append("```\n"); | ||
|
|
||
| return sb.toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Serializes an object to JSON using the injected {@link ObjectMapper}. | ||
| * Falls back to {@link Object#toString()} if serialization fails. | ||
| */ | ||
| private String serialize(Object obj) { | ||
| try { | ||
| return objectMapper.writeValueAsString(obj); | ||
| } catch (Exception e) { | ||
| return obj.toString(); | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use ```json markdown code fencing (also for other JSON lists / objects)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All three data sections (history, blackboard objects, existing memories) now use
```jsonmarkdown code fencing with proper JSON arrays. Commit 3d6dda0.