From 82a33c390979aaf1c9280c608b4ec4baf3cf1624 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Fri, 19 Jun 2026 22:51:33 +0800 Subject: [PATCH] feat(impression): add after-rolling updater pipeline --- .../core/cognition/CognitionCapability.java | 7 +- .../cognition/impression/ImpressionCore.java | 18 +- .../ImpressionUpdateApplyResult.java | 16 + .../impression/ImpressionUpdateContext.java | 19 ++ .../ImpressionUpdatePlanApplier.java | 96 ++++++ .../ImpressionUpdatePlanValidator.java | 71 ++++ .../impression/ImpressionUpdatePlanner.java | 78 +++++ .../module/impression/ImpressionUpdater.java | 92 +++++ .../CommunicationProducerTest.java | 7 +- .../impression/ImpressionUpdaterTest.java | 323 ++++++++++++++++++ .../memory/runtime/MemoryRuntimeTest.java | 7 +- 11 files changed, 727 insertions(+), 7 deletions(-) create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateApplyResult.java create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateContext.java create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanApplier.java create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanValidator.java create mode 100644 Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanner.java create mode 100644 Partner-Core/src/test/java/work/slhaf/partner/module/impression/ImpressionUpdaterTest.java diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java index 870251d6..4957bc09 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/CognitionCapability.java @@ -45,13 +45,18 @@ public interface CognitionCapability { /** * Create and register a new known entity by subject, then refresh search indexes for it. */ - Entity createEntity(String subject); + String createEntity(String subject); /** * Return a known entity by uuid, or null when it does not exist. */ Entity getEntity(String uuid); + /** + * Activate a known entity into the runtime context and return a detached active-entity snapshot. + */ + ActiveEntity activateKnownEntity(String entityUuid); + /** * Bind an active runtime entity to a known entity and refresh the active-entity search document. */ diff --git a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java index f490aba8..496b4d11 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/core/cognition/impression/ImpressionCore.java @@ -103,7 +103,7 @@ public class ImpressionCore implements StateSerializable { * Create a new known entity and make it visible to recall/update indexes immediately. */ @CapabilityMethod - public Entity createEntity(String subject) { + public String createEntity(String subject) { if (subject == null || subject.isBlank()) { throw new IllegalArgumentException("subject must not be blank"); } @@ -112,7 +112,7 @@ public class ImpressionCore implements StateSerializable { entity.register(); knownEntitiesByUuid.put(entity.getUuid(), entity); refreshKnownEntityIndexes(entity); - return entity; + return entity.getUuid(); } /** @@ -126,6 +126,16 @@ public class ImpressionCore implements StateSerializable { return knownEntitiesByUuid.get(uuid); } + /** + * Activate a known entity and return a detached snapshot for external consumers. + */ + @CapabilityMethod + public ActiveEntity activateKnownEntity(String entityUuid) { + return activateKnownEntityLive(entityUuid) + .map(ActiveEntity::snapshot) + .orElse(null); + } + /** * Bind a runtime active entity to a known entity. * This keeps the active entity in current context while giving later updates a stable storage target. @@ -328,7 +338,7 @@ public class ImpressionCore implements StateSerializable { private Optional resolveActiveEntity(ImpressionSearchTarget target) { return switch (target.getType()) { case ACTIVE_ENTITY -> findActiveEntityByRuntimeId(target.getId()); - case ENTITY -> activateKnownEntity(target.getId()); + case ENTITY -> activateKnownEntityLive(target.getId()); }; } @@ -348,7 +358,7 @@ public class ImpressionCore implements StateSerializable { } } - private Optional activateKnownEntity(String uuid) { + private Optional activateKnownEntityLive(String uuid) { Entity knownEntity = knownEntitiesByUuid.get(uuid); if (knownEntity == null) { return Optional.empty(); diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateApplyResult.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateApplyResult.java new file mode 100644 index 00000000..1e0a422b --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateApplyResult.java @@ -0,0 +1,16 @@ +package work.slhaf.partner.module.impression; + +import java.util.List; + +public record ImpressionUpdateApplyResult( + List createdEntityUuids +) { + + public ImpressionUpdateApplyResult { + createdEntityUuids = createdEntityUuids == null ? List.of() : List.copyOf(createdEntityUuids); + } + + public static ImpressionUpdateApplyResult empty() { + return new ImpressionUpdateApplyResult(List.of()); + } +} diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateContext.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateContext.java new file mode 100644 index 00000000..e9a0c162 --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdateContext.java @@ -0,0 +1,19 @@ +package work.slhaf.partner.module.impression; + +import work.slhaf.partner.framework.agent.model.pojo.Message; + +import java.util.List; + +public record ImpressionUpdateContext( + String memoryUnitId, + String memorySliceId, + String summary, + int rollingSize, + int retainDivisor, + int sliceStartIndex, + int sliceEndIndex, + long sliceTimestamp, + long unitTimestamp, + List incrementMessages +) { +} diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanApplier.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanApplier.java new file mode 100644 index 00000000..601dc583 --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanApplier.java @@ -0,0 +1,96 @@ +package work.slhaf.partner.module.impression; + +import work.slhaf.partner.core.cognition.CognitionCapability; +import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability; +import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; +import work.slhaf.partner.framework.agent.support.Result; + +import java.util.ArrayList; +import java.util.List; + +public class ImpressionUpdatePlanApplier extends AbstractAgentModule.Sub> { + + @InjectCapability + private CognitionCapability cognitionCapability; + + @Override + protected Result doExecute(ImpressionUpdatePlan plan) { + return apply(plan); + } + + public Result apply(ImpressionUpdatePlan plan) { + return Result.runCatching(() -> { + if (plan == null || plan.getStatus() != PlanStatus.CONFIRMED) { + throw new IllegalArgumentException("only confirmed impression update plans can be applied"); + } + List createdEntityUuids = new ArrayList<>(); + for (ImpressionUpdateStep step : plan.getSteps()) { + String createdEntityUuid = applyStep(step); + if (createdEntityUuid != null && !createdEntityUuid.isBlank()) { + createdEntityUuids.add(createdEntityUuid); + } + } + return new ImpressionUpdateApplyResult(createdEntityUuids); + }); + } + + private String applyStep(ImpressionUpdateStep step) { + if (step instanceof UpdateExistingStep updateStep) { + applyPatch(updateStep.getEntityUuid(), updateStep.getUpdatePatch()); + return null; + } + if (step instanceof CreateEntityStep createStep) { + String entityUuid = cognitionCapability.createEntity(createStep.getSubject()); + if (entityUuid == null || entityUuid.isBlank()) { + throw new IllegalStateException("created entity uuid is blank"); + } + applyPatches(entityUuid, createStep.getImpressions()); + applyPatches(entityUuid, createStep.getFeatures()); + applyPatches(entityUuid, createStep.getAliases()); + applyPatches(entityUuid, createStep.getRelations()); + return entityUuid; + } + throw new IllegalArgumentException("unsupported impression update step: " + step); + } + + private void applyPatches(String entityUuid, List patches) { + for (UpdatePatch patch : patches) { + applyPatch(entityUuid, patch); + } + } + + private void applyPatch(String entityUuid, UpdatePatch patch) { + boolean applied = switch (patch) { + case SubjectPatch subjectPatch -> cognitionCapability.renameEntitySubject( + entityUuid, + subjectPatch.getSubject(), + subjectPatch.getKeepOldSubjectAsAlias() + ); + case AliasPatch aliasPatch -> cognitionCapability.addEntityAlias(entityUuid, aliasPatch.getAlias(), aliasPatch.getDeprecated()); + case ImpressionPatch impressionPatch -> cognitionCapability.updateEntityImpression( + entityUuid, + impressionPatch.getImpression(), + impressionPatch.getNewImpression(), + impressionPatch.getConfidence() + ); + case FeaturePatch featurePatch -> cognitionCapability.updateEntityFeature( + entityUuid, + featurePatch.getFeature(), + featurePatch.getNewFeature(), + featurePatch.getConfidence() + ); + case RelationPatch relationPatch -> cognitionCapability.updateEntityRelation( + entityUuid, + relationPatch.getTarget(), + relationPatch.getRelation(), + relationPatch.getStrength() + ); + case null, default -> throw new IllegalArgumentException("unsupported impression update patch: " + patch); + }; + + if (!applied) { + throw new IllegalStateException("failed to apply impression update patch: " + patch); + } + } + +} diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanValidator.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanValidator.java new file mode 100644 index 00000000..3815327d --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanValidator.java @@ -0,0 +1,71 @@ +package work.slhaf.partner.module.impression; + +import org.jetbrains.annotations.NotNull; +import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; + +import java.util.List; + +public class ImpressionUpdatePlanValidator extends AbstractAgentModule.Sub { + + @Override + protected @NotNull Boolean doExecute(ImpressionUpdatePlan plan) { + return isExecutable(plan); + } + + public boolean isExecutable(ImpressionUpdatePlan plan) { + if (plan == null || plan.getStatus() != PlanStatus.PREPARED) { + return false; + } + List steps = plan.getSteps(); + if (steps.isEmpty()) { + return false; + } + for (ImpressionUpdateStep step : steps) { + if (!isValidStep(step)) { + return false; + } + } + return true; + } + + private boolean isValidStep(ImpressionUpdateStep step) { + if (step instanceof UpdateExistingStep updateStep) { + return hasText(updateStep.getEntityUuid()) && isValidPatch(updateStep.getUpdatePatch()); + } + if (step instanceof CreateEntityStep createStep) { + return hasText(createStep.getSubject()) + && (!createStep.getImpressions().isEmpty() + || !createStep.getFeatures().isEmpty() + || !createStep.getAliases().isEmpty() + || !createStep.getRelations().isEmpty()) + && createStep.getImpressions().stream().allMatch(this::isValidPatch) + && createStep.getFeatures().stream().allMatch(this::isValidPatch) + && createStep.getAliases().stream().allMatch(this::isValidPatch) + && createStep.getRelations().stream().allMatch(this::isValidPatch); + } + return false; + } + + private boolean isValidPatch(UpdatePatch patch) { + if (patch instanceof ImpressionPatch impressionPatch) { + return hasText(impressionPatch.getImpression()); + } + if (patch instanceof FeaturePatch featurePatch) { + return hasText(featurePatch.getFeature()); + } + if (patch instanceof AliasPatch aliasPatch) { + return hasText(aliasPatch.getAlias()); + } + if (patch instanceof SubjectPatch subjectPatch) { + return hasText(subjectPatch.getSubject()); + } + if (patch instanceof RelationPatch relationPatch) { + return hasText(relationPatch.getTarget()) && hasText(relationPatch.getRelation()); + } + return false; + } + + private boolean hasText(String value) { + return value != null && !value.isBlank(); + } +} diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanner.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanner.java new file mode 100644 index 00000000..35f88778 --- /dev/null +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdatePlanner.java @@ -0,0 +1,78 @@ +package work.slhaf.partner.module.impression; + +import kotlin.Unit; +import org.jetbrains.annotations.NotNull; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import work.slhaf.partner.framework.agent.exception.AgentRuntimeException; +import work.slhaf.partner.framework.agent.exception.ModuleExecutionException; +import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; +import work.slhaf.partner.framework.agent.model.ActivateModel; +import work.slhaf.partner.framework.agent.model.pojo.Message; +import work.slhaf.partner.framework.agent.support.Result; +import work.slhaf.partner.module.TaskBlock; + +import java.util.List; + +public class ImpressionUpdatePlanner extends AbstractAgentModule.Sub> implements ActivateModel { + + private static final String MODULE_PROMPT = """ + 你负责在对话 rolling 后,根据新的 memory slice 证据生成保守的实体印象更新计划。 + + 你只输出 ImpressionUpdatePlan 对应结构: + - 如果没有稳定、可复用的实体信息变化,返回 REJECTED 并说明原因。 + - 只有当证据明确支持时,才返回 PREPARED 计划来创建实体或更新已有实体。 + - 不要做复杂实体合并,不要发明不在证据中的事实。 + - patch 字段必须使用简洁、稳定、可索引的表达。 + - 不要输出 CONFIRMED;CONFIRMED 只能由代码 Validator 通过后设置。 + """; + + @Override + protected Result doExecute(ImpressionUpdateContext context) { + return plan(context); + } + + public Result plan(ImpressionUpdateContext context) { + try { + return Result.success(formattedChat(List.of(buildTaskMessage(context)), ImpressionUpdatePlan.class).getOrThrow()); + } catch (AgentRuntimeException e) { + return Result.failure(new ModuleExecutionException( + "planning impression update failed", + this.getClass(), + getModuleName() + )); + } + } + + private Message buildTaskMessage(ImpressionUpdateContext context) { + return new TaskBlock("impression_update_task") { + @Override + protected void fillXml(@NotNull Document document, @NotNull Element root) { + appendTextElement(document, root, "memory_unit_id", context.memoryUnitId()); + appendTextElement(document, root, "memory_slice_id", context.memorySliceId()); + appendTextElement(document, root, "summary", context.summary()); + appendTextElement(document, root, "rolling_size", Integer.toString(context.rollingSize())); + appendTextElement(document, root, "retain_divisor", Integer.toString(context.retainDivisor())); + appendTextElement(document, root, "slice_start_index", Integer.toString(context.sliceStartIndex())); + appendTextElement(document, root, "slice_end_index", Integer.toString(context.sliceEndIndex())); + appendTextElement(document, root, "slice_timestamp", Long.toString(context.sliceTimestamp())); + appendTextElement(document, root, "unit_timestamp", Long.toString(context.unitTimestamp())); + appendListElement(document, root, "increment_messages", "message", context.incrementMessages(), (element, message) -> { + element.setAttribute("role", message.roleValue()); + element.setTextContent(message.getContent()); + return Unit.INSTANCE; + }); + } + }.encodeToMessage(); + } + + @Override + public @NotNull String modelKey() { + return "impression_update_planner"; + } + + @Override + public @NotNull List modulePrompt() { + return List.of(new Message(Message.Character.SYSTEM, MODULE_PROMPT)); + } +} diff --git a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java index 68af7652..e7f2ae90 100644 --- a/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java +++ b/Partner-Core/src/main/java/work/slhaf/partner/module/impression/ImpressionUpdater.java @@ -1,19 +1,111 @@ package work.slhaf.partner.module.impression; +import org.jetbrains.annotations.NotNull; +import org.w3c.dom.Document; +import org.w3c.dom.Element; import work.slhaf.partner.core.cognition.CognitionCapability; +import work.slhaf.partner.core.cognition.context.BlockContent; +import work.slhaf.partner.core.cognition.context.ContextBlock; +import work.slhaf.partner.core.cognition.impression.ActiveEntity; +import work.slhaf.partner.core.memory.pojo.MemorySliceSnapshot; +import work.slhaf.partner.core.memory.pojo.MemoryUnitSnapshot; import work.slhaf.partner.framework.agent.factory.capability.annotation.InjectCapability; import work.slhaf.partner.framework.agent.factory.component.abstracts.AbstractAgentModule; +import work.slhaf.partner.framework.agent.factory.component.annotation.Init; +import work.slhaf.partner.framework.agent.factory.component.annotation.InjectModule; +import work.slhaf.partner.framework.agent.support.Result; import work.slhaf.partner.module.communication.AfterRolling; +import work.slhaf.partner.module.communication.AfterRollingRegistry; import work.slhaf.partner.module.communication.RollingResult; +import java.util.List; +import java.util.Set; + public class ImpressionUpdater extends AbstractAgentModule.Standalone implements AfterRolling { @InjectCapability private CognitionCapability cognitionCapability; + @InjectModule + private AfterRollingRegistry afterRollingRegistry; + @InjectModule + private ImpressionUpdatePlanner planner; + @InjectModule + private ImpressionUpdatePlanValidator validator; + @InjectModule + private ImpressionUpdatePlanApplier applier; + + @Init + public void init() { + afterRollingRegistry.register(this); + } @Override public void consume(RollingResult result) { + ImpressionUpdateContext context = buildContext(result); + Result planResult = planner.execute(context); + ImpressionUpdatePlan plan = planResult.getOrDefault(null); + if (!validator.execute(plan)) { + return; + } + ImpressionUpdatePlan confirmedPlan = new ImpressionUpdatePlan( + plan.getSteps(), + PlanStatus.CONFIRMED, + plan.getReason() + ); + Result applyResult = applier.execute(confirmedPlan); + applyResult.onFailure(exp -> applierFailure(context, exp.getMessage())) + .onSuccess(applySummary -> applySummary.createdEntityUuids().forEach(entityUuid -> { + ActiveEntity activeEntity = cognitionCapability.activateKnownEntity(entityUuid); + if (activeEntity != null) { + registerActiveEntity(activeEntity); + } + })); + } + private ImpressionUpdateContext buildContext(RollingResult result) { + MemoryUnitSnapshot unit = result.getMemoryUnit(); + MemorySliceSnapshot slice = result.getMemorySlice(); + return new ImpressionUpdateContext( + unit.getId(), + slice.getId(), + result.getSummary(), + result.getRollingSize(), + result.getRetainDivisor(), + slice.getStartIndex(), + slice.getEndIndex(), + slice.getTimestamp(), + unit.getTimestamp(), + List.copyOf(result.incrementMessages()) + ); + } + + private void applierFailure(ImpressionUpdateContext context, String message) { + cognitionCapability.contextWorkspace().register(new ContextBlock( + new BlockContent("impression_update_apply_failure", "impression_updater", BlockContent.Urgency.LOW) { + @Override + protected void fillXml(@NotNull Document document, @NotNull Element root) { + appendTextElement(document, root, "memory_unit_id", context.memoryUnitId()); + appendTextElement(document, root, "memory_slice_id", context.memorySliceId()); + appendTextElement(document, root, "message", message == null ? "" : message); + } + }, + Set.of(ContextBlock.FocusedDomain.COGNITION), + 20, + 20, + 0 + )); + } + + private void registerActiveEntity(ActiveEntity activeEntity) { + cognitionCapability.contextWorkspace().register(new ContextBlock( + activeEntity, + activeEntity, + activeEntity, + Set.of(ContextBlock.FocusedDomain.COGNITION, ContextBlock.FocusedDomain.MEMORY), + 100, + 0.5, + 20 + )); } } diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java index 40b58237..32dea96d 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/communication/CommunicationProducerTest.java @@ -160,7 +160,7 @@ class CommunicationProducerTest { } @Override - public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { + public String createEntity(String subject) { return null; } @@ -169,6 +169,11 @@ class CommunicationProducerTest { return null; } + @Override + public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) { + return null; + } + @Override public boolean bindActiveEntity(String runtimeId, String entityUuid) { return false; diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/impression/ImpressionUpdaterTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/impression/ImpressionUpdaterTest.java new file mode 100644 index 00000000..f1ecf172 --- /dev/null +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/impression/ImpressionUpdaterTest.java @@ -0,0 +1,323 @@ +package work.slhaf.partner.module.impression; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.w3c.dom.Element; +import work.slhaf.partner.core.cognition.CognitionCapability; +import work.slhaf.partner.core.cognition.context.ContextWorkspace; +import work.slhaf.partner.core.cognition.impression.ActiveEntity; +import work.slhaf.partner.core.cognition.impression.Entity; +import work.slhaf.partner.core.memory.pojo.MemorySlice; +import work.slhaf.partner.core.memory.pojo.MemoryUnit; +import work.slhaf.partner.framework.agent.model.pojo.Message; +import work.slhaf.partner.framework.agent.support.Result; +import work.slhaf.partner.module.communication.AfterRollingRegistry; +import work.slhaf.partner.module.communication.RollingResult; + +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +class ImpressionUpdaterTest { + + @BeforeAll + static void beforeAll(@TempDir Path tempDir) { + System.setProperty("user.home", tempDir.toAbsolutePath().toString()); + } + + private static void setField(Object target, String fieldName, Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static RollingResult rollingResult() { + String unitId = "impression-updater-test-" + UUID.randomUUID(); + MemoryUnit unit = new MemoryUnit(unitId); + unit.getConversationMessages().addAll(List.of( + new Message(Message.Character.USER, "user likes quiet tools"), + new Message(Message.Character.ASSISTANT, "noted") + )); + MemorySlice slice = new MemorySlice(0, 2, "summary"); + unit.getSlices().add(slice); + return new RollingResult(unit.snapshot(), slice.snapshot(), 2, 6); + } + + private static ImpressionUpdatePlan plan(PlanStatus status, ImpressionUpdateStep... steps) { + return new ImpressionUpdatePlan(List.of(steps), status, null); + } + + @Test + void shouldRegisterItselfToAfterRollingRegistryOnInit() throws Exception { + ImpressionUpdater updater = new ImpressionUpdater(); + AfterRollingRegistry registry = mock(AfterRollingRegistry.class); + setField(updater, "afterRollingRegistry", registry); + + updater.init(); + + verify(registry).register(updater); + } + + @Test + void shouldNotApplyEmptyPlan() throws Exception { + TestApplier applier = new TestApplier(Result.success(ImpressionUpdateApplyResult.empty())); + ImpressionUpdater updater = updaterWith(plan(PlanStatus.PREPARED), applier, new RecordingCognitionCapability()); + + updater.consume(rollingResult()); + + assertEquals(0, applier.applyCount); + } + + @Test + void shouldNotApplyRejectedOrInvalidPlan() throws Exception { + TestApplier applier = new TestApplier(Result.success(ImpressionUpdateApplyResult.empty())); + ImpressionUpdater updater = updaterWith( + plan(PlanStatus.REJECTED, new UpdateExistingStep("entity-1", new ImpressionPatch("stable"))), + applier, + new RecordingCognitionCapability() + ); + + updater.consume(rollingResult()); + + assertEquals(0, applier.applyCount); + } + + @Test + void shouldApplyConfirmedUpdateExistingPlanThroughMutationApi() throws Exception { + RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability(); + ImpressionUpdatePlanApplier applier = new ImpressionUpdatePlanApplier(); + setField(applier, "cognitionCapability", cognitionCapability); + + Result result = applier.apply(plan( + PlanStatus.CONFIRMED, + new UpdateExistingStep("entity-1", new ImpressionPatch("old impression", "new impression", 0.7)) + )); + + assertNull(result.exceptionOrNull()); + assertEquals("entity-1", cognitionCapability.lastImpressionEntityUuid); + assertEquals("old impression", cognitionCapability.lastImpression); + assertEquals("new impression", cognitionCapability.lastNewImpression); + assertEquals(0.7, cognitionCapability.lastConfidence); + } + + @Test + void shouldCreateEntityApplyPatchesActivateAndRegisterActiveSnapshot() throws Exception { + RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability(); + cognitionCapability.createdEntityUuid = "entity-created"; + ImpressionUpdatePlanApplier applier = new ImpressionUpdatePlanApplier(); + setField(applier, "cognitionCapability", cognitionCapability); + + Result result = applier.apply(plan( + PlanStatus.CONFIRMED, + new CreateEntityStep( + "User", + List.of(new ImpressionPatch("prefers concise updates")), + List.of(), + List.of(new AliasPatch("operator")), + List.of() + ) + )); + + assertNull(result.exceptionOrNull()); + assertEquals("User", cognitionCapability.createdSubject); + assertEquals("entity-created", cognitionCapability.lastImpressionEntityUuid); + assertEquals("entity-created", cognitionCapability.lastAliasEntityUuid); + assertEquals(List.of("entity-created"), result.getOrThrow().createdEntityUuids()); + } + + @Test + void shouldConfirmValidatedPreparedPlanAndRegisterCreatedActiveEntity() throws Exception { + RecordingCognitionCapability cognitionCapability = new RecordingCognitionCapability(); + ActiveEntity activeEntity = new ActiveEntity("runtime-1"); + activeEntity.updateSubject("User"); + activeEntity.bindEntity("entity-created"); + cognitionCapability.activatedEntity = activeEntity.snapshot(); + TestApplier applier = new TestApplier(Result.success(new ImpressionUpdateApplyResult(List.of("entity-created")))); + ImpressionUpdater updater = updaterWith( + plan(PlanStatus.PREPARED, new CreateEntityStep( + "User", + List.of(new ImpressionPatch("prefers concise updates")), + List.of(), + List.of(), + List.of() + )), + applier, + cognitionCapability + ); + + updater.consume(rollingResult()); + + assertEquals(1, applier.applyCount); + assertEquals(PlanStatus.CONFIRMED, applier.lastPlanStatus); + assertEquals("entity-created", cognitionCapability.activatedEntityUuid); + String resolvedXml = cognitionCapability.contextWorkspace() + .resolve(List.of(work.slhaf.partner.core.cognition.context.ContextBlock.FocusedDomain.COGNITION)) + .encodeToMessage() + .getContent(); + assertTrue(resolvedXml.contains("active_entity_runtime-1")); + } + + private static ImpressionUpdater updaterWith(ImpressionUpdatePlan plan, + ImpressionUpdatePlanApplier applier, + CognitionCapability cognitionCapability) throws Exception { + ImpressionUpdater updater = new ImpressionUpdater(); + setField(updater, "cognitionCapability", cognitionCapability); + setField(updater, "planner", new TestPlanner(Result.success(plan))); + setField(updater, "validator", new ImpressionUpdatePlanValidator()); + setField(updater, "applier", applier); + return updater; + } + + private static class TestPlanner extends ImpressionUpdatePlanner { + private final Result result; + + private TestPlanner(Result result) { + this.result = result; + } + + @Override + public Result plan(ImpressionUpdateContext context) { + return result; + } + } + + private static class TestApplier extends ImpressionUpdatePlanApplier { + private final Result result; + private int applyCount; + private PlanStatus lastPlanStatus; + + private TestApplier(Result result) { + this.result = result; + } + + @Override + public Result apply(ImpressionUpdatePlan plan) { + applyCount++; + lastPlanStatus = plan.getStatus(); + return result; + } + } + + private static class RecordingCognitionCapability implements CognitionCapability { + private final ContextWorkspace contextWorkspace = new ContextWorkspace(); + private final Lock lock = new ReentrantLock(); + private String createdEntityUuid = "entity-1"; + private String createdSubject; + private String lastImpressionEntityUuid; + private String lastImpression; + private String lastNewImpression; + private double lastConfidence; + private String lastAliasEntityUuid; + private String activatedEntityUuid; + private ActiveEntity activatedEntity; + + @Override + public void initiateTurn(String input, String target, String... skippedModules) { + } + + @Override + public ContextWorkspace contextWorkspace() { + return contextWorkspace; + } + + @Override + public List getChatMessages() { + return List.of(); + } + + @Override + public List snapshotChatMessages() { + return List.of(); + } + + @Override + public void rollChatMessagesWithSnapshot(int snapshotSize, int retainDivisor) { + } + + @Override + public void refreshRecentChatMessagesContext() { + } + + @Override + public Element messageNotesElement() { + return null; + } + + @Override + public Lock getMessageLock() { + return lock; + } + + @Override + public Set projectEntity(String input) { + return Set.of(); + } + + @Override + public Map showEntities() { + return Map.of(); + } + + @Override + public String createEntity(String subject) { + createdSubject = subject; + return createdEntityUuid; + } + + @Override + public Entity getEntity(String uuid) { + return null; + } + + @Override + public ActiveEntity activateKnownEntity(String entityUuid) { + activatedEntityUuid = entityUuid; + return activatedEntity; + } + + @Override + public boolean bindActiveEntity(String runtimeId, String entityUuid) { + return true; + } + + @Override + public boolean renameEntitySubject(String entityUuid, String newSubject, boolean keepOldSubjectAsAlias) { + return true; + } + + @Override + public boolean addEntityAlias(String entityUuid, String alias, boolean deprecated) { + lastAliasEntityUuid = entityUuid; + return true; + } + + @Override + public boolean updateEntityImpression(String entityUuid, String impression, String newImpression, double confidence) { + lastImpressionEntityUuid = entityUuid; + lastImpression = impression; + lastNewImpression = newImpression; + lastConfidence = confidence; + return true; + } + + @Override + public boolean updateEntityFeature(String entityUuid, String feature, String newFeature, double confidence) { + return true; + } + + @Override + public boolean updateEntityRelation(String entityUuid, String target, String relation, double strength) { + return true; + } + } +} diff --git a/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java b/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java index 0f3de692..1a039f8d 100644 --- a/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java +++ b/Partner-Core/src/test/java/work/slhaf/partner/module/memory/runtime/MemoryRuntimeTest.java @@ -111,7 +111,7 @@ class MemoryRuntimeTest { } @Override - public work.slhaf.partner.core.cognition.impression.Entity createEntity(String subject) { + public String createEntity(String subject) { return null; } @@ -120,6 +120,11 @@ class MemoryRuntimeTest { return null; } + @Override + public work.slhaf.partner.core.cognition.impression.ActiveEntity activateKnownEntity(String entityUuid) { + return null; + } + @Override public boolean bindActiveEntity(String runtimeId, String entityUuid) { return false;