Add MemoryDistillerAgent for post-completion memory distillation#335
Conversation
…ocess - Create MemoryDistillationResult record with stored/revoked memory lists - Create MemoryDistillerAgent with @agent, @action, @AchievesGoal annotations using ai().withDefaultLlm().createObject() for LLM-based memory distillation - Add maxMemories parameter to KafkaAgenticAggregateRuntime constructor - Integrate memory distillation in resumeNextAgentTask: when AgentProcess finishes with COMPLETED status, run MemoryDistillerAgent to completion using run() and translate results into MemoryStoredEvent/MemoryRevokedEvent - Enforce net memory limit: stored - revoked <= maxMemories - currentCount - Handle gracefully: missing agent, null result, exceptions - Add comprehensive tests for distillation logic and limit enforcement Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/b0b7907c-8588-4b46-af3a-67b710ba24ad Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/b0b7907c-8588-4b46-af3a-67b710ba24ad Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds post-completion “memory distillation” to the agentic runtime: when an AgentProcess finishes COMPLETED, a dedicated Embabel agent is run to propose MemoryStoredEvent / MemoryRevokedEvent updates, constrained by the aggregate’s maxMemories.
Changes:
- Integrates memory distillation into
KafkaAgenticAggregateRuntime.resumeNextAgentTask()forCOMPLETEDprocesses, with max-memory enforcement. - Introduces
MemoryDistillerAgent+ structured output recordMemoryDistillationResult. - Updates runtime factory wiring and expands/adjusts unit tests for the new constructor and distillation behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntime.java | Adds maxMemories configuration and runs a distiller agent on COMPLETED processes; translates result into memory events with capacity enforcement. |
| main/agentic/src/main/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillerAgent.java | New Embabel agent that builds an LLM prompt from process context to produce a MemoryDistillationResult. |
| main/agentic/src/main/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillationResult.java | New structured LLM output record for stored/revoked memories. |
| main/agentic/src/main/java/org/elasticsoftware/akces/agentic/beans/AgenticAggregateRuntimeFactory.java | Wires @AgenticAggregateInfo.maxMemories() into the runtime constructor. |
| main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/MemoryDistillationTest.java | New unit tests for distillation integration, limits, skipping behavior, and bindings. |
| main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/ResumeNextAgentTaskTest.java | Updates runtime construction to pass maxMemories. |
| main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntimeTest.java | Updates runtime construction to pass maxMemories. |
| main/agentic/src/test/java/org/elasticsoftware/akces/agentic/runtime/AgenticAggregateAutoCreateTest.java | Updates runtime construction to pass maxMemories. |
| main/agentic/src/test/java/org/elasticsoftware/akces/agentic/embabel/MemoryDistillationResultTest.java | Adds tests for the new MemoryDistillationResult record. |
| List<?> history = blackboard.objectsOfType(ActionInvocation.class); | ||
| List<?> blackboardObjects = (List<?>) blackboard.get("blackboardObjects"); | ||
| List<?> existingMemories = (List<?>) blackboard.get("existingMemories"); | ||
| int maxNewMemories = (int) blackboard.get("maxNewMemories"); |
There was a problem hiding this comment.
distillMemories builds a "history" binding (see runtime), but this agent ignores it and instead reads blackboard.objectsOfType(ActionInvocation.class). Unless the distiller process itself adds ActionInvocation objects, the prompt will likely always show “(no actions recorded)”. Read the history binding from the blackboard (and fall back safely if it’s missing) so the distiller sees the completed process history as intended.
| List<?> history = blackboard.objectsOfType(ActionInvocation.class); | |
| List<?> blackboardObjects = (List<?>) blackboard.get("blackboardObjects"); | |
| List<?> existingMemories = (List<?>) blackboard.get("existingMemories"); | |
| int maxNewMemories = (int) blackboard.get("maxNewMemories"); | |
| Object historyBinding = blackboard.get("history"); | |
| List<?> history = historyBinding instanceof List<?> historyList | |
| ? historyList | |
| : blackboard.objectsOfType(ActionInvocation.class); | |
| Object blackboardObjectsBinding = blackboard.get("blackboardObjects"); | |
| List<?> blackboardObjects = blackboardObjectsBinding instanceof List<?> blackboardObjectList | |
| ? blackboardObjectList | |
| : List.of(); | |
| Object existingMemoriesBinding = blackboard.get("existingMemories"); | |
| List<?> existingMemories = existingMemoriesBinding instanceof List<?> memoryList | |
| ? memoryList | |
| : List.of(); | |
| Object maxNewMemoriesBinding = blackboard.get("maxNewMemories"); | |
| int maxNewMemories = maxNewMemoriesBinding instanceof Number number | |
| ? number.intValue() | |
| : 0; |
| sb.append("\nBLACKBOARD OBJECTS (process context and results):\n"); | ||
| if (blackboardObjects != null && !blackboardObjects.isEmpty()) { | ||
| for (Object obj : blackboardObjects) { | ||
| sb.append("- [").append(obj.getClass().getSimpleName()).append("] ") | ||
| .append(obj).append("\n"); | ||
| } | ||
| } else { |
There was a problem hiding this comment.
This prompt construction serializes all blackboard objects (via obj.toString()) and sends them to the default LLM. That can exfiltrate secrets/PII or large internal payloads to an external model provider and can also blow up prompt size. Consider whitelisting/summarizing only safe + bounded fields, redacting sensitive patterns, and/or enforcing a strict size limit before sending content to the LLM.
There was a problem hiding this comment.
don't use obj.toString() here but use an (injected) ObjectMapper. Annotations can be used to avoid serializing dangerous information and exposing it (outside the scope). Use this approach for all data. Also take a look at how Embabel exposes the Blackboard to a LLM
There was a problem hiding this comment.
Injected ObjectMapper via constructor and now use objectMapper.writeValueAsString(obj) for all serialization (history, blackboard objects, and existing memories). Falls back to toString() only if serialization fails. Commit 3103bbc.
There was a problem hiding this comment.
Injected ObjectMapper via constructor and now use objectMapper.writeValueAsString(obj) for all data serialization in the prompt. Falls back to toString() only if serialization fails. Commit 3103bbc.
| // Distill memories from successfully completed processes | ||
| if (statusCode == AgentProcessStatusCode.COMPLETED) { | ||
| List<DomainEvent> memoryEvents = distillMemories(agentProcess, state); | ||
| if (!memoryEvents.isEmpty()) { | ||
| tickEvents = Stream.concat(tickEvents, memoryEvents.stream()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| delegate.processDomainEvents(tickEvents, task.agentProcessId(), protocolRecordConsumer, stateRecordSupplier); | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------------- | ||
| // Memory distillation | ||
| // ------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Distills relevant memories from a successfully completed {@link AgentProcess} | ||
| * by running the {@link MemoryDistillerAgent} to completion. | ||
| * | ||
| * <p>Creates a separate agent process for the {@link MemoryDistillerAgent}, populates | ||
| * its blackboard with the completed process's history and blackboard objects, the | ||
| * current memories, and the maximum number of net new memories allowed, then runs | ||
| * the process to completion via {@link AgentProcess#run()}. | ||
| * | ||
| * <p>The net memory constraint ensures that | ||
| * {@code storedCount - revokedCount <= maxMemories - currentMemoryCount}, preventing | ||
| * the memory system from exceeding its configured capacity. | ||
| * | ||
| * @param completedProcess the agent process that has completed successfully | ||
| * @param state the current aggregate state | ||
| * @return a list of {@link MemoryStoredEvent} and {@link MemoryRevokedEvent} instances; | ||
| * may be empty if no memories need to be changed | ||
| */ | ||
| private List<DomainEvent> distillMemories(AgentProcess completedProcess, AggregateState state) { | ||
| Agent memoryDistillerAgent = resolveMemoryDistillerAgent(); | ||
| if (memoryDistillerAgent == null) { | ||
| logger.debug("MemoryDistillerAgent not deployed on the platform; skipping memory distillation"); | ||
| return List.of(); | ||
| } | ||
|
|
||
| List<AgenticAggregateMemory> currentMemories = state instanceof MemoryAwareState mas | ||
| ? mas.getMemories() | ||
| : List.of(); | ||
| int maxNewMemories = Math.max(0, maxMemories - currentMemories.size()); | ||
|
|
||
| Map<String, Object> bindings = new LinkedHashMap<>(); | ||
| bindings.put("history", completedProcess.getHistory()); | ||
| bindings.put("blackboardObjects", completedProcess.getBlackboard().getObjects()); | ||
| bindings.put("existingMemories", currentMemories); | ||
| bindings.put("maxNewMemories", maxNewMemories); | ||
|
|
||
| try { | ||
| AgentProcess distillerProcess = agentPlatform.createAgentProcess( | ||
| memoryDistillerAgent, ProcessOptions.DEFAULT, bindings); | ||
| distillerProcess.run(); | ||
|
|
There was a problem hiding this comment.
resumeNextAgentTask is invoked inside AgenticAggregatePartition.resumeAgentTasks() after producer.beginTransaction(); running distillerProcess.run() here performs a full (potentially slow) LLM call while holding an open Kafka transaction. This can significantly increase transaction duration and risk timeouts/abort under load. Consider moving memory distillation outside the Kafka transaction (e.g., enqueue a follow-up command/event for distillation), or make it non-blocking / bounded with timeouts so it can’t stall the poll/transaction loop.
| if (storedCount >= maxStoredCount) { | ||
| logger.debug("Memory distillation limit reached ({} stored, {} revoked, max {}); " | ||
| + "truncating remaining stored memories for aggregate {}", | ||
| storedCount, revokedCount, maxNewMemories, getName()); |
There was a problem hiding this comment.
The debug log message uses maxNewMemories for the “max” placeholder, but the truncation threshold is maxStoredCount (= maxNewMemories + revokedCount). This makes the log misleading when revocations increase capacity. Log maxStoredCount (and/or both values) so the message reflects the actual enforced limit.
| storedCount, revokedCount, maxNewMemories, getName()); | |
| storedCount, revokedCount, maxStoredCount, getName()); |
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| /** | ||
| * Unit tests for {@link MemoryDistillationResult}. | ||
| */ | ||
| class MemoryDistillationResultTest { | ||
|
|
||
| @Test | ||
| void shouldCreateWithStoredAndRevokedMemories() { | ||
| var stored = List.of( | ||
| new MemoryDistillationResult.StoredMemory("testing", "Use JUnit 5", "src/test", "best practice"), | ||
| new MemoryDistillationResult.StoredMemory("logging", "Use SLF4J", "src/main", "consistency") | ||
| ); | ||
| var revoked = List.of( | ||
| new MemoryDistillationResult.RevokedMemory("mem-old-1", "superseded by new info") | ||
| ); | ||
|
|
||
| var result = new MemoryDistillationResult(stored, revoked); | ||
|
|
||
| assertThat(result.stored()).hasSize(2); | ||
| assertThat(result.revoked()).hasSize(1); | ||
| assertThat(result.stored().getFirst().subject()).isEqualTo("testing"); | ||
| assertThat(result.stored().getFirst().fact()).isEqualTo("Use JUnit 5"); | ||
| assertThat(result.revoked().getFirst().memoryId()).isEqualTo("mem-old-1"); | ||
| } | ||
|
|
||
| @Test | ||
| void shouldCreateWithEmptyLists() { | ||
| var result = new MemoryDistillationResult(List.of(), List.of()); | ||
|
|
||
| assertThat(result.stored()).isEmpty(); | ||
| assertThat(result.revoked()).isEmpty(); | ||
| } | ||
|
|
||
| @Test | ||
| void storedMemoryShouldExposeAllFields() { | ||
| var stored = new MemoryDistillationResult.StoredMemory( | ||
| "conventions", "Use records for DTOs", "src/main/Model.java:10", "immutability"); | ||
|
|
||
| assertThat(stored.subject()).isEqualTo("conventions"); | ||
| assertThat(stored.fact()).isEqualTo("Use records for DTOs"); | ||
| assertThat(stored.citations()).isEqualTo("src/main/Model.java:10"); | ||
| assertThat(stored.reason()).isEqualTo("immutability"); | ||
| } | ||
|
|
||
| @Test | ||
| void revokedMemoryShouldExposeAllFields() { | ||
| var revoked = new MemoryDistillationResult.RevokedMemory("mem-123", "outdated"); | ||
|
|
||
| assertThat(revoked.memoryId()).isEqualTo("mem-123"); | ||
| assertThat(revoked.reason()).isEqualTo("outdated"); | ||
| } |
There was a problem hiding this comment.
This test class is exercising the autogenerated accessors of a Java record in isolation. These tests tend to be brittle/low-signal (the JVM already guarantees record component accessors) and increase maintenance without validating any Akces behavior. Prefer removing these tests or folding any meaningful assertions into tests that cover the record’s usage in the runtime integration.
| import org.junit.jupiter.api.Test; | |
| import java.util.List; | |
| import static org.assertj.core.api.Assertions.assertThat; | |
| /** | |
| * Unit tests for {@link MemoryDistillationResult}. | |
| */ | |
| class MemoryDistillationResultTest { | |
| @Test | |
| void shouldCreateWithStoredAndRevokedMemories() { | |
| var stored = List.of( | |
| new MemoryDistillationResult.StoredMemory("testing", "Use JUnit 5", "src/test", "best practice"), | |
| new MemoryDistillationResult.StoredMemory("logging", "Use SLF4J", "src/main", "consistency") | |
| ); | |
| var revoked = List.of( | |
| new MemoryDistillationResult.RevokedMemory("mem-old-1", "superseded by new info") | |
| ); | |
| var result = new MemoryDistillationResult(stored, revoked); | |
| assertThat(result.stored()).hasSize(2); | |
| assertThat(result.revoked()).hasSize(1); | |
| assertThat(result.stored().getFirst().subject()).isEqualTo("testing"); | |
| assertThat(result.stored().getFirst().fact()).isEqualTo("Use JUnit 5"); | |
| assertThat(result.revoked().getFirst().memoryId()).isEqualTo("mem-old-1"); | |
| } | |
| @Test | |
| void shouldCreateWithEmptyLists() { | |
| var result = new MemoryDistillationResult(List.of(), List.of()); | |
| assertThat(result.stored()).isEmpty(); | |
| assertThat(result.revoked()).isEmpty(); | |
| } | |
| @Test | |
| void storedMemoryShouldExposeAllFields() { | |
| var stored = new MemoryDistillationResult.StoredMemory( | |
| "conventions", "Use records for DTOs", "src/main/Model.java:10", "immutability"); | |
| assertThat(stored.subject()).isEqualTo("conventions"); | |
| assertThat(stored.fact()).isEqualTo("Use records for DTOs"); | |
| assertThat(stored.citations()).isEqualTo("src/main/Model.java:10"); | |
| assertThat(stored.reason()).isEqualTo("immutability"); | |
| } | |
| @Test | |
| void revokedMemoryShouldExposeAllFields() { | |
| var revoked = new MemoryDistillationResult.RevokedMemory("mem-123", "outdated"); | |
| assertThat(revoked.memoryId()).isEqualTo("mem-123"); | |
| assertThat(revoked.reason()).isEqualTo("outdated"); | |
| } | |
| /** | |
| * Dedicated unit tests for {@link MemoryDistillationResult} were removed because they only | |
| * exercised compiler-generated Java record accessors in isolation. Any meaningful verification | |
| * of this type should happen through higher-level tests that cover its runtime usage. | |
| */ | |
| class MemoryDistillationResultTest { |
| // Collect valid revocations first (only for memories that actually exist) | ||
| Set<String> currentMemoryIds = new HashSet<>(); | ||
| for (AgenticAggregateMemory mem : currentMemories) { | ||
| currentMemoryIds.add(mem.memoryId()); | ||
| } | ||
|
|
||
| List<MemoryDistillationResult.RevokedMemory> validRevocations = new ArrayList<>(); | ||
| if (result.revoked() != null) { | ||
| for (MemoryDistillationResult.RevokedMemory revoked : result.revoked()) { | ||
| if (revoked.memoryId() != null && currentMemoryIds.contains(revoked.memoryId())) { | ||
| validRevocations.add(revoked); | ||
| } else { | ||
| logger.debug("Skipping revocation of non-existent memory '{}' for aggregate {}", | ||
| revoked.memoryId(), getName()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Calculate how many stored memories we can accept | ||
| int revokedCount = validRevocations.size(); |
There was a problem hiding this comment.
maxStoredCount is derived from validRevocations.size(), but validRevocations can include duplicate memoryIds. Duplicate revocations typically become no-ops at apply time, which would let the code accept too many stored memories and potentially exceed maxMemories. De-duplicate revocations by memoryId (and base revokedCount on unique, actually-present IDs) before calculating the allowed stored count.
| // Collect valid revocations first (only for memories that actually exist) | |
| Set<String> currentMemoryIds = new HashSet<>(); | |
| for (AgenticAggregateMemory mem : currentMemories) { | |
| currentMemoryIds.add(mem.memoryId()); | |
| } | |
| List<MemoryDistillationResult.RevokedMemory> validRevocations = new ArrayList<>(); | |
| if (result.revoked() != null) { | |
| for (MemoryDistillationResult.RevokedMemory revoked : result.revoked()) { | |
| if (revoked.memoryId() != null && currentMemoryIds.contains(revoked.memoryId())) { | |
| validRevocations.add(revoked); | |
| } else { | |
| logger.debug("Skipping revocation of non-existent memory '{}' for aggregate {}", | |
| revoked.memoryId(), getName()); | |
| } | |
| } | |
| } | |
| // Calculate how many stored memories we can accept | |
| int revokedCount = validRevocations.size(); | |
| // Collect valid revocations first (only for memories that actually exist), | |
| // de-duplicated by memoryId so capacity calculations match effective revocations. | |
| Set<String> currentMemoryIds = new HashSet<>(); | |
| for (AgenticAggregateMemory mem : currentMemories) { | |
| currentMemoryIds.add(mem.memoryId()); | |
| } | |
| List<MemoryDistillationResult.RevokedMemory> validRevocations = new ArrayList<>(); | |
| Set<String> revokedMemoryIds = new HashSet<>(); | |
| if (result.revoked() != null) { | |
| for (MemoryDistillationResult.RevokedMemory revoked : result.revoked()) { | |
| String memoryId = revoked.memoryId(); | |
| if (memoryId == null || !currentMemoryIds.contains(memoryId)) { | |
| logger.debug("Skipping revocation of non-existent memory '{}' for aggregate {}", | |
| memoryId, getName()); | |
| continue; | |
| } | |
| if (!revokedMemoryIds.add(memoryId)) { | |
| logger.debug("Skipping duplicate revocation of memory '{}' for aggregate {}", | |
| memoryId, getName()); | |
| continue; | |
| } | |
| validRevocations.add(revoked); | |
| } | |
| } | |
| // Calculate how many stored memories we can accept | |
| int revokedCount = revokedMemoryIds.size(); |
| return new KafkaAgenticAggregateRuntime( | ||
| kafkaRuntime, objectMapper, agenticInfo.stateClass(), agentPlatform, aggregate); | ||
| kafkaRuntime, objectMapper, agenticInfo.stateClass(), agentPlatform, aggregate, | ||
| agenticInfo.maxMemories()); |
There was a problem hiding this comment.
We need an extra param in the agentic info annotation: maxMemoriesAdded, rename the current maxMemories param to MaxTotalMemories. Ensure both are passes into the MemoryDistillerAgent
There was a problem hiding this comment.
Added maxMemoriesAdded (default 10) and renamed maxMemories to maxTotalMemories in @AgenticAggregateInfo. Both values are passed into the MemoryDistillerAgent via blackboard bindings (maxTotalMemories and maxMemoriesAdded). The effective limit is min(capacityLeft, maxMemoriesAdded). Commit 6aa01aa.
| * @param revoked the list of memories to revoke; each entry contains memoryId and reason | ||
| */ | ||
| public record MemoryDistillationResult( | ||
| List<StoredMemory> stored, |
There was a problem hiding this comment.
use the MemoryStoredEvent directly, not need for intermediate DTO
There was a problem hiding this comment.
MemoryDistillationResult now uses List<MemoryStoredEvent> directly. Commit 6aa01aa.
| */ | ||
| public record MemoryDistillationResult( | ||
| List<StoredMemory> stored, | ||
| List<RevokedMemory> revoked |
There was a problem hiding this comment.
use the MemoryRevokedEvent directly
There was a problem hiding this comment.
MemoryDistillationResult now uses List<MemoryRevokedEvent> directly. Commit 6aa01aa.
| * The well-known agent name used to locate this agent on the | ||
| * {@link com.embabel.agent.core.AgentPlatform}. | ||
| */ | ||
| public static final String AGENT_NAME = "MemoryDistillerAgent"; |
There was a problem hiding this comment.
rename to MemoryDistiller (not the Class). Also apply the same pattern to the exsiting DefaultAgent
There was a problem hiding this comment.
Renamed AGENT_NAME to "MemoryDistiller" in MemoryDistillerAgent and to "Default" in DefaultAgent (same pattern). Commit 6aa01aa.
| .createObject(prompt, MemoryDistillationResult.class); | ||
| } | ||
|
|
||
| private String buildPrompt(List<?> history, |
There was a problem hiding this comment.
use multi-line String here for readability
There was a problem hiding this comment.
Replaced the StringBuilder.append() chains with Java text blocks (multi-line strings) for readability. Commit 6aa01aa.
| private List<DomainEvent> distillMemories(AgentProcess completedProcess, AggregateState state) { | ||
| Agent memoryDistillerAgent = resolveMemoryDistillerAgent(); | ||
| if (memoryDistillerAgent == null) { | ||
| logger.debug("MemoryDistillerAgent not deployed on the platform; skipping memory distillation"); |
There was a problem hiding this comment.
make this a warning, this cannot happen so something is wrongly configured. probably path scanning
There was a problem hiding this comment.
Changed to logger.warn(...). Commit 6aa01aa.
…e agent names, multi-line prompt, warn log - Rename maxMemories to maxTotalMemories and add maxMemoriesAdded in @AgenticAggregateInfo - Use MemoryStoredEvent/MemoryRevokedEvent directly in MemoryDistillationResult (no intermediate DTOs) - Rename agent names: DefaultAgent → "Default", MemoryDistillerAgent → "MemoryDistiller" - Use multi-line String (text blocks) in MemoryDistillerAgent prompt building - Change "not deployed" log level from DEBUG to WARN - Pass both maxTotalMemories and maxMemoriesAdded to distiller bindings - De-duplicate revocations by memoryId - Add safe binding reads with instanceof checks - Update all tests for new signatures and types Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/40e6b583-f254-41d2-95c4-718e34edd19e Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
| @Test | ||
| void getMemoriesShouldReturnEmptyListForNonMemoryAwareState() throws IOException { | ||
| var plainRuntime = new KafkaAgenticAggregateRuntime(delegate, objectMapper, PlainState.class, agentPlatform, aggregate); | ||
| var plainRuntime = new KafkaAgenticAggregateRuntime(delegate, objectMapper, PlainState.class, agentPlatform, aggregate, 100, 10); |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| */ | ||
| @Agent(name = MemoryDistillerAgent.AGENT_NAME, | ||
| description = "Distills relevant memories from a completed agent process", | ||
| planner = PlannerType.UTILITY) |
There was a problem hiding this comment.
since we have an AchievesGoal we should use GOAP here
There was a problem hiding this comment.
Changed to PlannerType.GOAP in commit 3103bbc.
| sb.append("\nBLACKBOARD OBJECTS (process context and results):\n"); | ||
| if (blackboardObjects != null && !blackboardObjects.isEmpty()) { | ||
| for (Object obj : blackboardObjects) { | ||
| sb.append("- [").append(obj.getClass().getSimpleName()).append("] ") | ||
| .append(obj).append("\n"); | ||
| } | ||
| } else { |
There was a problem hiding this comment.
don't use obj.toString() here but use an (injected) ObjectMapper. Annotations can be used to avoid serializing dangerous information and exposing it (outside the scope). Use this approach for all data. Also take a look at how Embabel exposes the Blackboard to a LLM
| sb.append("\nEXISTING MEMORIES:\n"); | ||
| if (existingMemories != null && !existingMemories.isEmpty()) { | ||
| for (Object memory : existingMemories) { | ||
| sb.append("- ").append(memory).append("\n"); |
There was a problem hiding this comment.
use ObjectMapper to serialize
There was a problem hiding this comment.
Now using objectMapper.writeValueAsString(memory) to serialize existing memories. Commit 3103bbc.
|
|
||
| sb.append(""" | ||
|
|
||
| Return a JSON object with 'stored' (list of MemoryStoredEvent instances) \ |
There was a problem hiding this comment.
see how Embabel ensure that the LLM knows what to return. I think this is already handled by calling .createObject on the prompt
There was a problem hiding this comment.
Removed the explicit return format instructions from the prompt. The .createObject(prompt, MemoryDistillationResult.class) call already provides the LLM with the required schema. Commit 3103bbc.
… return format - Change MemoryDistillerAgent planner from UTILITY to GOAP (has @AchievesGoal) - Inject ObjectMapper via constructor and use it for JSON serialization of history, blackboard objects, and existing memories in the prompt - Remove explicit return format instructions from prompt since .createObject() already instructs the LLM on the return schema - Fix maxMemories → maxTotalMemories bug in constructor validation - Add maxMemoriesAdded >= 0 validation Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/c67ccc3c-4bc2-4df9-84d7-9a3c6fbc9a46 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/c67ccc3c-4bc2-4df9-84d7-9a3c6fbc9a46 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
| } | ||
|
|
||
| sb.append("\nBLACKBOARD OBJECTS (process context and results):\n"); | ||
| if (blackboardObjects != null && !blackboardObjects.isEmpty()) { |
There was a problem hiding this comment.
use ```json markdown code fencing (also for other JSON lists / objects)
There was a problem hiding this comment.
All three data sections (history, blackboard objects, existing memories) now use ```json markdown code fencing with proper JSON arrays. Commit 3d6dda0.
| } | ||
|
|
||
| sb.append("\nEXISTING MEMORIES:\n"); | ||
| if (existingMemories != null && !existingMemories.isEmpty()) { |
There was a problem hiding this comment.
should be a JSON list with markdown fencing
There was a problem hiding this comment.
Existing memories now rendered as a JSON array with ```json markdown fencing. Commit 3d6dda0.
| * Set to a low value to avoid overwhelming the memory store with many entries in a single | ||
| * agent run. | ||
| */ | ||
| int maxMemoriesAdded() default 10; |
| * exceeds this limit the oldest entries are evicted to make room for new ones. | ||
| */ | ||
| int maxMemories() default 100; | ||
| int maxTotalMemories() default 100; |
There was a problem hiding this comment.
the default max memories should be 50
…Memories=50, maxMemoriesAdded=5 Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/7180951b-dc17-4c5f-89fe-515a51c44b07 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
jwijgerd
left a comment
There was a problem hiding this comment.
there are test failures, fix them
| ? mas.getMemories() | ||
| : List.of(); | ||
|
|
||
| Map<String, Object> bindings = new LinkedHashMap<>(); |
There was a problem hiding this comment.
also put the AgentTask on the blackboard
There was a problem hiding this comment.
Added agentTask to the distiller blackboard bindings. The MemoryDistillerAgent now reads it and includes it in the LLM prompt as a json-fenced section. The bindings test verifies the new binding. Commit c8ec319.
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/2e093a9b-24ea-4985-8829-5188dc97827f Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
Fixed in c8ec319. The |
| public MemoryDistillationResult distillMemories(ActionContext context) { | ||
| var blackboard = context.getProcessContext().getAgentProcess().getBlackboard(); | ||
|
|
||
| Object agentTaskBinding = blackboard.get("agentTask"); |
There was a problem hiding this comment.
instead of reading from the blackboard use String binding in the action method params. The embabel framework should inject the correct values
There was a problem hiding this comment.
Created a strongly-typed MemoryDistillationInput record that wraps all distillation inputs (agentTask, history, blackboardObjects, existingMemories, maxTotalMemories, maxMemoriesAdded). The action method now takes MemoryDistillationInput input as a parameter — Embabel injects it from the Blackboard via type-based resolution. No more manual blackboard.get(...) calls. Commit 6358a91.
…d params Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/a03c329c-ac6e-4e7c-8779-1b7a390b1f5f Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
| } else { | ||
| sb.append("[]\n"); | ||
| } | ||
| sb.append("```\n"); |
There was a problem hiding this comment.
aren't we missing a closing bracket here to close the json list?
There was a problem hiding this comment.
The closing ] bracket is present on line 163 (sb.append("]\n")) inside the if-block, before the markdown fence closing on line 167. The empty case on line 165 also outputs []. The generated prompt correctly produces a valid JSON array for the BLACKBOARD OBJECTS section.
|
|
||
| sb.append("\nEXISTING MEMORIES:\n```json\n"); | ||
| if (existingMemories != null && !existingMemories.isEmpty()) { | ||
| sb.append("[\n"); |
There was a problem hiding this comment.
check if we are not missing the closing bracket
There was a problem hiding this comment.
The closing ] bracket is present on line 177 (sb.append("]\n")) inside the if-block. The empty case on line 179 also outputs []. The EXISTING MEMORIES section is correctly bracket-closed.
Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/0d443301-e294-4183-adf3-3c217d7c5007 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com>
MemoryDistillationInput— strongly-typed record for Embabel type-based Blackboard injectionMemoryDistillerAgent— GOAP planner, ObjectMapper, JSON markdown fencing, no explicit return formatMemoryDistillationResult— usesMemoryStoredEvent/MemoryRevokedEventdirectly@AgenticAggregateInfo—maxTotalMemories(default 50),maxMemoriesAdded(default 5)DefaultAgent.AGENT_NAME="Default",MemoryDistillerAgent.AGENT_NAME="MemoryDistiller"aggregateIdparam fromtranslateDistillationResult— all log messages already usegetName()MemoryDistillerAgentprompt builder