From 648eac41aa56b2163e999bfbcd33e92a7d064ac7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:32:38 +0000 Subject: [PATCH 1/3] Add description field to AggregateServiceRecord and contained types Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/971d76bd-afa2-4835-8e58-6d5693b9f894 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com> --- .../aggregate-service-record-descriptions.md | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 plans/aggregate-service-record-descriptions.md diff --git a/plans/aggregate-service-record-descriptions.md b/plans/aggregate-service-record-descriptions.md new file mode 100644 index 00000000..5b34fe75 --- /dev/null +++ b/plans/aggregate-service-record-descriptions.md @@ -0,0 +1,191 @@ +# Plan: Add `description` Field to `AggregateServiceRecord` and Contained Types + +## Overview + +To make the Agentic Layer more descriptive, the `AggregateServiceRecord` and its contained types +(`AggregateServiceCommandType`, `AggregateServiceDomainEventType`) need to expose human-readable +description strings. These descriptions are already declared by developers in the existing `*Info` +annotations (`@AggregateInfo`, `@AgenticAggregateInfo`, `@CommandInfo`, `@DomainEventInfo`); they +simply need to be propagated into the control record that is published to the `Akces-Control` Kafka +topic and consumed by service-discovery clients (including the Agentic Layer). + +## Current State + +| Type | Location | Has `description`? | +|---|---|---| +| `@AggregateInfo` | `main/api` | ✅ `description()` default `""` | +| `@AgenticAggregateInfo` | `main/api` | ✅ `description()` default `""` | +| `@CommandInfo` | `main/api` | ✅ `description()` default `""` | +| `@DomainEventInfo` | `main/api` | ✅ `description()` default `""` | +| `AggregateServiceRecord` | `main/shared` | ❌ missing | +| `AggregateServiceCommandType` | `main/shared` | ❌ missing | +| `AggregateServiceDomainEventType` | `main/shared` | ❌ missing | + +The descriptions are read from annotations at runtime during control-record publication, so they +will be available via `typeClass().getAnnotation(...)`. + +## Affected Modules + +- **`main/shared`** – records `AggregateServiceRecord`, `AggregateServiceCommandType`, + `AggregateServiceDomainEventType` +- **`main/runtime`** – `AggregateRuntime` interface, `KafkaAggregateRuntime`, + `AkcesAggregateController.publishControlRecord()` +- **`main/agentic`** – `AkcesAgenticAggregateController.publishControlRecord()` +- **Tests** – unit/integration tests that construct these records directly + +## Architectural Decisions + +### 1. Backward Compatibility + +The `description` field is nullable (or missing from legacy JSON). Jackson deserializes unknown +fields gracefully and missing fields default to `null`. No migration of existing Kafka records is +needed. Consumers must treat `null` description as "no description available". + +### 2. Description Source + +| Record field | Source annotation | Source field | +|---|---|---| +| `AggregateServiceRecord.description` | `@AggregateInfo` or `@AgenticAggregateInfo` | `.description()` | +| `AggregateServiceCommandType.description` | `@CommandInfo` on command class | `.description()` | +| `AggregateServiceDomainEventType.description` | `@DomainEventInfo` on event class | `.description()` | + +For `AggregateServiceRecord`, the description is read from the aggregate class annotation. This +requires exposing `getDescription()` on the `AggregateRuntime` interface and implementing it in +`KafkaAggregateRuntime`. The implementation reads the `@AggregateInfo` or `@AgenticAggregateInfo` +annotation from the aggregate class stored in the runtime. + +For `AggregateServiceCommandType` and `AggregateServiceDomainEventType`, the description is +extracted at publish time directly from the `typeClass()` field of the `CommandType` / +`DomainEventType` objects (which are available in the publisher). + +### 3. Empty vs. Null + +When the annotation `description()` returns `""` (the default), the stored value in the record +will be `null` (normalized). This keeps the JSON clean for the common case where no description is +provided. A helper method (or inline ternary) will be used at population time: +```java +String desc = annotation.description(); +String normalized = (desc == null || desc.isBlank()) ? null : desc; +``` + +### 4. Built-in / System Types in Agentic Controller + +The `AkcesAgenticAggregateController` also constructs `AggregateServiceCommandType` and +`AggregateServiceDomainEventType` for built-in types (`StoreMemoryCommand`, +`ForgetMemoryCommand`, and built-in events). These will also pick up descriptions from their +`@CommandInfo` / `@DomainEventInfo` annotations. + +--- + +## Phases + +### Phase 1 – Extend `main/shared` Control Records + +**Scope:** `main/shared` module only. + +1. Add `@Nullable String description` as the **last** component to `AggregateServiceRecord`. + - Keep existing constructor order; append `description` at the end to minimise diff. + - Use `@tools.jackson.annotation.JsonInclude(NON_NULL)` to suppress `null` in JSON output. + - Update the JavaDoc `@param` block. + +2. Add `@Nullable String description` as the **last** component to `AggregateServiceCommandType`. + - Same JSON treatment. + - Update JavaDoc. + +3. Add `@Nullable String description` as the **last** component to + `AggregateServiceDomainEventType`. + - Same JSON treatment. + - Update JavaDoc. + +**Backward compatibility note:** Adding a new nullable field to a Java `record` is a breaking +change in the constructor signature. All construction sites must be updated (see Phase 3). +Deserialization of old JSON (without the field) continues to work because Jackson uses the +`@JsonProperty`-annotated constructor parameters and unknown/missing fields are ignored. + +--- + +### Phase 2 – Add `getDescription()` to `AggregateRuntime` / `KafkaAggregateRuntime` + +**Scope:** `main/runtime` module. + +1. Add `String getDescription()` to the `AggregateRuntime` interface (returns `null` when no + description is set). +2. Implement `getDescription()` in `KafkaAggregateRuntime`: + - The aggregate class is stored in the `aggregateClass` field. + - Read `@AggregateInfo.description()` (or `@AgenticAggregateInfo.description()` for agentic + runtimes) and normalize to `null` when blank. + +--- + +### Phase 3 – Populate Descriptions in `AkcesAggregateController` (Standard Aggregates) + +**Scope:** `main/runtime` module, `AkcesAggregateController.publishControlRecord()`. + +Update the three construction sites: + +1. **`AggregateServiceRecord`** – pass `aggregateRuntime.getDescription()`. +2. **`AggregateServiceCommandType`** (produced-commands stream) – extract description from + `commandType.typeClass().getAnnotation(CommandInfo.class)`. +3. **`AggregateServiceDomainEventType`** (produced-events stream) – extract description from + `domainEventType.typeClass().getAnnotation(DomainEventInfo.class)`. +4. **`AggregateServiceDomainEventType`** (consumed-events stream) – same as above. + +--- + +### Phase 4 – Populate Descriptions in `AkcesAgenticAggregateController` (Agentic Aggregates) + +**Scope:** `main/agentic` module, `AkcesAgenticAggregateController.publishControlRecord()`. + +Mirrors Phase 3 for the agentic controller. Built-in command/event types already have `@CommandInfo` +/ `@DomainEventInfo` annotations with their descriptions (or the defaults), so they will be handled +uniformly. + +--- + +### Phase 5 – Update Tests + +**Scope:** All modules with tests that construct these records directly. + +Files to update (identified by static analysis): + +| File | Change | +|---|---| +| `main/runtime/src/test/.../control/AkcesAggregateControllerTests.java` | Add `null` description argument(s) to record constructors | +| `main/runtime/src/test/.../RuntimeTests.java` | Same | +| `main/client/src/test/.../AkcesClientTests.java` | Same | +| `main/query-support/src/test/.../TestUtils.java` | JSON strings are deserialized — no change to JSON needed; new field simply deserializes as `null`. Verify deserialization still works. | + +Tests that use JSON deserialization (query-support `TestUtils.java`) require no JSON changes; +legacy JSON without a `description` field will deserialize correctly with `description = null`. + +--- + +## GitHub Issues + +The following issues need to be created (in dependency order), with the last one being the tracking +issue: + +1. **Issue – Phase 1**: "Add `description` field to `AggregateServiceRecord` and contained types" + - Scope: `main/shared` + - No dependencies + +2. **Issue – Phase 2**: "Add `getDescription()` to `AggregateRuntime` interface and + `KafkaAggregateRuntime`" + - Scope: `main/runtime` + - Depends on: Issue 1 + +3. **Issue – Phase 3**: "Populate `description` in `AkcesAggregateController.publishControlRecord()`" + - Scope: `main/runtime` + - Depends on: Issues 1, 2 + +4. **Issue – Phase 4**: "Populate `description` in `AkcesAgenticAggregateController.publishControlRecord()`" + - Scope: `main/agentic` + - Depends on: Issues 1, 2 + +5. **Issue – Phase 5**: "Update tests for `AggregateServiceRecord` description field" + - Scope: `main/runtime`, `main/client`, `main/agentic` + - Depends on: Issues 1, 3, 4 + +6. **Tracking Issue**: "Propagate `*Info` annotation descriptions into `AggregateServiceRecord`" + - Links all above issues + - Describes the overall goal for the Agentic Layer From 1da1a645b9a92bf9af8cb9e0fc28f8fac8354997 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:53:21 +0000 Subject: [PATCH 2/3] Add description field to AggregateServiceRecord, AggregateServiceCommandType, AggregateServiceDomainEventType, and propagate from *Info annotations - Add nullable description field to AggregateServiceRecord (from @AggregateInfo/@AgenticAggregateInfo) - Add nullable description field to AggregateServiceCommandType (from @CommandInfo) - Add nullable description field to AggregateServiceDomainEventType (from @DomainEventInfo) - Add getDescription() to AggregateRuntime interface - Implement getDescription() in KafkaAggregateRuntime - Add normalizeDescription() utility (blank -> null) - Delegate getDescription() in KafkaAgenticAggregateRuntime - Populate descriptions in AkcesAggregateController.publishControlRecord() - Populate descriptions in AkcesAgenticAggregateController.publishControlRecord() - Update test constructors in AkcesAggregateControllerTests, RuntimeTests, AkcesClientTests Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/155e062c-8ac3-4b61-a3dd-f44e26b2bf04 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com> --- .../AkcesAgenticAggregateController.java | 21 ++++++++++----- .../runtime/KafkaAgenticAggregateRuntime.java | 7 +++++ .../akces/client/AkcesClientTests.java | 7 ++--- .../akces/AkcesAggregateController.java | 12 ++++++--- .../akces/aggregate/AggregateRuntime.java | 10 +++++++ .../akces/kafka/KafkaAggregateRuntime.java | 27 +++++++++++++++++++ .../akcestest/RuntimeTests.java | 7 ++--- .../AkcesAggregateControllerTests.java | 14 +++++----- .../control/AggregateServiceCommandType.java | 22 ++++++++++++++- .../AggregateServiceDomainEventType.java | 24 ++++++++++++++++- .../akces/control/AggregateServiceRecord.java | 11 +++++++- 11 files changed, 138 insertions(+), 24 deletions(-) diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java index 8b3ec603..dd57cfe7 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java @@ -30,6 +30,8 @@ import org.elasticsoftware.akces.agentic.commands.StoreMemoryCommand; import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent; import org.elasticsoftware.akces.agentic.events.MemoryStoredEvent; +import org.elasticsoftware.akces.annotations.CommandInfo; +import org.elasticsoftware.akces.annotations.DomainEventInfo; import org.elasticsoftware.akces.aggregate.CommandType; import org.elasticsoftware.akces.aggregate.DomainEventType; import org.elasticsoftware.akces.control.AggregateServiceCommandType; @@ -72,6 +74,7 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static org.elasticsoftware.akces.kafka.KafkaAggregateRuntime.normalizeDescription; import static org.elasticsoftware.akces.kafka.PartitionUtils.COMMANDS_SUFFIX; import static org.elasticsoftware.akces.kafka.PartitionUtils.DOMAINEVENTS_SUFFIX; @@ -431,22 +434,26 @@ private void publishControlRecord() { BUILTIN_COMMAND_TYPES.forEach(ct -> allCommands.add(new AggregateServiceCommandType( ct.typeName(), ct.version(), ct.create(), - "commands." + ct.typeName()))); + "commands." + ct.typeName(), + normalizeDescription(ct.typeClass().getAnnotation(CommandInfo.class).description())))); aggregateRuntime.getLocalCommandTypes().forEach(ct -> allCommands.add(new AggregateServiceCommandType( ct.typeName(), ct.version(), ct.create(), - "commands." + ct.typeName()))); + "commands." + ct.typeName(), + normalizeDescription(ct.typeClass().getAnnotation(CommandInfo.class).description())))); // Combine built-in + aggregate event types List allEvents = new ArrayList<>(); BUILTIN_EVENT_TYPES.forEach(et -> allEvents.add(new AggregateServiceDomainEventType( et.typeName(), et.version(), et.create(), et.external(), - "domainevents." + et.typeName()))); + "domainevents." + et.typeName(), + normalizeDescription(et.typeClass().getAnnotation(DomainEventInfo.class).description())))); aggregateRuntime.getProducedDomainEventTypes().forEach(et -> allEvents.add(new AggregateServiceDomainEventType( et.typeName(), et.version(), et.create(), et.external(), - "domainevents." + et.typeName()))); + "domainevents." + et.typeName(), + normalizeDescription(et.typeClass().getAnnotation(DomainEventInfo.class).description())))); // Consumed external events — mirrors the pattern in AkcesAggregateController so // that service discovery correctly reflects the external event dependencies. @@ -454,7 +461,8 @@ private void publishControlRecord() { aggregateRuntime.getExternalDomainEventTypes().stream() .map(et -> new AggregateServiceDomainEventType( et.typeName(), et.version(), et.create(), et.external(), - "domainevents." + et.typeName())) + "domainevents." + et.typeName(), + normalizeDescription(et.typeClass().getAnnotation(DomainEventInfo.class).description()))) .toList(); AggregateServiceRecord serviceRecord = new AggregateServiceRecord( @@ -464,7 +472,8 @@ private void publishControlRecord() { AggregateServiceType.AGENTIC, allCommands, allEvents, - consumedEvents + consumedEvents, + aggregateRuntime.getDescription() ); controlProducer.beginTransaction(); diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntime.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntime.java index 3597ea8b..4b627d00 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntime.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/KafkaAgenticAggregateRuntime.java @@ -18,6 +18,7 @@ package org.elasticsoftware.akces.agentic.runtime; import com.embabel.agent.core.AgentPlatform; +import jakarta.annotation.Nullable; import org.apache.kafka.common.errors.SerializationException; import org.elasticsoftware.akces.agentic.AgenticAggregateRuntime; import org.elasticsoftware.akces.agentic.events.MemoryRevokedEvent; @@ -115,6 +116,12 @@ public String getName() { return delegate.getName(); } + @Override + @Nullable + public String getDescription() { + return delegate.getDescription(); + } + @Override public Class> getAggregateClass() { return delegate.getAggregateClass(); diff --git a/main/client/src/test/java/org/elasticsoftware/akces/client/AkcesClientTests.java b/main/client/src/test/java/org/elasticsoftware/akces/client/AkcesClientTests.java index 41485a8d..3da811a7 100644 --- a/main/client/src/test/java/org/elasticsoftware/akces/client/AkcesClientTests.java +++ b/main/client/src/test/java/org/elasticsoftware/akces/client/AkcesClientTests.java @@ -289,9 +289,10 @@ public static void prepareExternalServices(String bootstrapServers) { "Account-Commands", "Account-DomainEvents", AggregateServiceType.STANDARD, - List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount")), - List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated")), - List.of()); + List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount", null)), + List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated", null)), + List.of(), + null); controlProducer.beginTransaction(); for (int partition = 0; partition < 3; partition++) { controlProducer.send(new ProducerRecord<>("Akces-Control", partition, "Account", aggregateServiceRecord)); diff --git a/main/runtime/src/main/java/org/elasticsoftware/akces/AkcesAggregateController.java b/main/runtime/src/main/java/org/elasticsoftware/akces/AkcesAggregateController.java index b898fe4e..7d53336d 100644 --- a/main/runtime/src/main/java/org/elasticsoftware/akces/AkcesAggregateController.java +++ b/main/runtime/src/main/java/org/elasticsoftware/akces/AkcesAggregateController.java @@ -37,6 +37,7 @@ import org.elasticsoftware.akces.aggregate.DomainEventType; import org.elasticsoftware.akces.aggregate.SchemaType; import org.elasticsoftware.akces.annotations.CommandInfo; +import org.elasticsoftware.akces.annotations.DomainEventInfo; import org.elasticsoftware.akces.commands.Command; import org.elasticsoftware.akces.control.*; import org.elasticsoftware.akces.gdpr.GDPRContextRepositoryFactory; @@ -75,6 +76,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.elasticsoftware.akces.AkcesControllerState.*; import static org.elasticsoftware.akces.gdpr.GDPRAnnotationUtils.hasPIIDataAnnotation; +import static org.elasticsoftware.akces.kafka.KafkaAggregateRuntime.normalizeDescription; import static org.elasticsoftware.akces.kafka.PartitionUtils.*; import static org.elasticsoftware.akces.util.KafkaUtils.createCompactedTopic; import static org.elasticsoftware.akces.util.KafkaUtils.getIndexTopicName; @@ -463,21 +465,25 @@ private void publishControlRecord(int partitions) { commandType.typeName(), commandType.version(), commandType.create(), - "commands." + commandType.typeName())).toList(), + "commands." + commandType.typeName(), + normalizeDescription(commandType.typeClass().getAnnotation(CommandInfo.class).description()))).toList(), aggregateRuntime.getProducedDomainEventTypes().stream().map(domainEventType -> new AggregateServiceDomainEventType( domainEventType.typeName(), domainEventType.version(), domainEventType.create(), domainEventType.external(), - "domainevents." + domainEventType.typeName())).toList(), + "domainevents." + domainEventType.typeName(), + normalizeDescription(domainEventType.typeClass().getAnnotation(DomainEventInfo.class).description()))).toList(), aggregateRuntime.getExternalDomainEventTypes().stream().map(externalDomainEventType -> new AggregateServiceDomainEventType( externalDomainEventType.typeName(), externalDomainEventType.version(), externalDomainEventType.create(), externalDomainEventType.external(), - "domainevents." + externalDomainEventType.typeName())).toList()); + "domainevents." + externalDomainEventType.typeName(), + normalizeDescription(externalDomainEventType.typeClass().getAnnotation(DomainEventInfo.class).description()))).toList(), + aggregateRuntime.getDescription()); controlProducer.beginTransaction(); for (int partition = 0; partition < partitions; partition++) { controlProducer.send(new ProducerRecord<>("Akces-Control", partition, aggregateRuntime.getName(), aggregateServiceRecord)); diff --git a/main/runtime/src/main/java/org/elasticsoftware/akces/aggregate/AggregateRuntime.java b/main/runtime/src/main/java/org/elasticsoftware/akces/aggregate/AggregateRuntime.java index 3c79e7ae..36d929c3 100644 --- a/main/runtime/src/main/java/org/elasticsoftware/akces/aggregate/AggregateRuntime.java +++ b/main/runtime/src/main/java/org/elasticsoftware/akces/aggregate/AggregateRuntime.java @@ -17,6 +17,7 @@ package org.elasticsoftware.akces.aggregate; +import jakarta.annotation.Nullable; import org.apache.kafka.common.errors.SerializationException; import org.elasticsoftware.akces.commands.Command; import org.elasticsoftware.akces.commands.CommandBus; @@ -37,6 +38,15 @@ public interface AggregateRuntime { String getName(); + /** + * Returns the human-readable description of this aggregate, as specified in + * the {@code @AggregateInfo} or {@code @AgenticAggregateInfo} annotation. + * + * @return the description, or {@code null} if none was specified + */ + @Nullable + String getDescription(); + Class> getAggregateClass(); void handleCommandRecord(CommandRecord commandRecord, diff --git a/main/runtime/src/main/java/org/elasticsoftware/akces/kafka/KafkaAggregateRuntime.java b/main/runtime/src/main/java/org/elasticsoftware/akces/kafka/KafkaAggregateRuntime.java index 7c6514b6..5e4371e3 100644 --- a/main/runtime/src/main/java/org/elasticsoftware/akces/kafka/KafkaAggregateRuntime.java +++ b/main/runtime/src/main/java/org/elasticsoftware/akces/kafka/KafkaAggregateRuntime.java @@ -20,6 +20,8 @@ import tools.jackson.core.JacksonException; import tools.jackson.databind.JsonNode; import tools.jackson.databind.ObjectMapper; +import org.elasticsoftware.akces.annotations.AgenticAggregateInfo; +import org.elasticsoftware.akces.annotations.AggregateInfo; import org.elasticsoftware.akces.schemas.JsonSchema; import jakarta.annotation.Nullable; import org.apache.kafka.common.errors.SerializationException; @@ -107,6 +109,31 @@ public String getName() { return stateType.typeName(); } + @Override + @Nullable + public String getDescription() { + AggregateInfo aggregateInfo = aggregateClass.getAnnotation(AggregateInfo.class); + if (aggregateInfo != null) { + return normalizeDescription(aggregateInfo.description()); + } + AgenticAggregateInfo agenticInfo = aggregateClass.getAnnotation(AgenticAggregateInfo.class); + if (agenticInfo != null) { + return normalizeDescription(agenticInfo.description()); + } + return null; + } + + /** + * Normalizes a description string by returning {@code null} for blank values. + * + * @param description the raw description from the annotation + * @return the description if non-blank, or {@code null} + */ + @Nullable + public static String normalizeDescription(@Nullable String description) { + return description == null || description.isBlank() ? null : description; + } + @Override public Class> getAggregateClass() { return aggregateClass; diff --git a/main/runtime/src/test/java/org/elasticsoftware/akcestest/RuntimeTests.java b/main/runtime/src/test/java/org/elasticsoftware/akcestest/RuntimeTests.java index 446575b1..05685a45 100644 --- a/main/runtime/src/test/java/org/elasticsoftware/akcestest/RuntimeTests.java +++ b/main/runtime/src/test/java/org/elasticsoftware/akcestest/RuntimeTests.java @@ -186,9 +186,10 @@ public static void prepareExternalServices(String bootstrapServers) { "Account" + COMMANDS_SUFFIX, "Account" + DOMAINEVENTS_SUFFIX, AggregateServiceType.STANDARD, - List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount")), - List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated")), - List.of()); + List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount", null)), + List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated", null)), + List.of(), + null); controlProducer.beginTransaction(); for (int partition = 0; partition < 3; partition++) { controlProducer.send(new ProducerRecord<>("Akces-Control", partition, "Account", aggregateServiceRecord)); diff --git a/main/runtime/src/test/java/org/elasticsoftware/akcestest/control/AkcesAggregateControllerTests.java b/main/runtime/src/test/java/org/elasticsoftware/akcestest/control/AkcesAggregateControllerTests.java index 536d07c1..03277916 100644 --- a/main/runtime/src/test/java/org/elasticsoftware/akcestest/control/AkcesAggregateControllerTests.java +++ b/main/runtime/src/test/java/org/elasticsoftware/akcestest/control/AkcesAggregateControllerTests.java @@ -41,9 +41,10 @@ public void testSerialization() { "Account-Commands", "Account-DomainEvents", AggregateServiceType.STANDARD, - List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount")), - List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated")), - Collections.emptyList()); + List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount", null)), + List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated", null)), + Collections.emptyList(), + null); assertNotNull(record); } @@ -86,9 +87,10 @@ public void testSerde() { "Account-Commands", "Account-DomainEvents", AggregateServiceType.STANDARD, - List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount")), - List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated")), - Collections.emptyList()); + List.of(new AggregateServiceCommandType("CreateAccount", 1, true, "commands.CreateAccount", null)), + List.of(new AggregateServiceDomainEventType("AccountCreated", 1, true, false, "domainevents.AccountCreated", null)), + Collections.emptyList(), + null); byte[] serialized = serde.serializer().serialize("Akces-Control", record); assertNotNull(serialized); diff --git a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceCommandType.java b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceCommandType.java index 07571c95..5b932619 100644 --- a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceCommandType.java +++ b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceCommandType.java @@ -17,17 +17,37 @@ package org.elasticsoftware.akces.control; +import jakarta.annotation.Nullable; import org.elasticsoftware.akces.aggregate.CommandType; import org.elasticsoftware.akces.commands.Command; import static org.elasticsoftware.akces.gdpr.GDPRAnnotationUtils.hasPIIDataAnnotation; +/** + * Describes a command type supported by an aggregate service, as published on the + * {@code Akces-Control} topic. + * + * @param typeName the logical name of the command type + * @param version the schema version of the command + * @param create whether this command creates a new aggregate instance + * @param schemaName the schema registry subject name for this command + * @param description a human-readable description of the command; may be {@code null} + */ public record AggregateServiceCommandType( String typeName, int version, boolean create, - String schemaName + String schemaName, + @Nullable String description ) { + /** + * Converts this service command type into a {@link CommandType} for use as an external + * command type within an aggregate runtime. + * + * @param typeClass the Java class of the command + * @param the command type + * @return a new {@link CommandType} instance + */ public CommandType toExternalCommandType(Class typeClass) { return new CommandType<>(typeName, version, typeClass, create, true, hasPIIDataAnnotation(typeClass)); } diff --git a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceDomainEventType.java b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceDomainEventType.java index f9ceb042..0e416323 100644 --- a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceDomainEventType.java +++ b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceDomainEventType.java @@ -17,18 +17,40 @@ package org.elasticsoftware.akces.control; +import jakarta.annotation.Nullable; import org.elasticsoftware.akces.aggregate.DomainEventType; import org.elasticsoftware.akces.events.DomainEvent; import static org.elasticsoftware.akces.gdpr.GDPRAnnotationUtils.hasPIIDataAnnotation; +/** + * Describes a domain-event type produced or consumed by an aggregate service, as + * published on the {@code Akces-Control} topic. + * + * @param typeName the logical name of the domain-event type + * @param version the schema version of the domain event + * @param create whether this event creates a new aggregate instance + * @param external whether this event originates from another aggregate + * @param schemaName the schema registry subject name for this domain event + * @param description a human-readable description of the domain event; may be {@code null} + */ public record AggregateServiceDomainEventType( String typeName, int version, boolean create, boolean external, - String schemaName + String schemaName, + @Nullable String description ) { + /** + * Converts this service domain-event type into a {@link DomainEventType} for use + * within an aggregate runtime. + * + * @param typeClass the Java class of the domain event + * @param error whether this event represents an error + * @param the domain-event type + * @return a new {@link DomainEventType} instance + */ public DomainEventType toLocalDomainEventType(Class typeClass, boolean error) { return new DomainEventType<>(typeName, version, typeClass, create, external, error, hasPIIDataAnnotation(typeClass)); } diff --git a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceRecord.java b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceRecord.java index 1d04035d..73598d12 100644 --- a/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceRecord.java +++ b/main/shared/src/main/java/org/elasticsoftware/akces/control/AggregateServiceRecord.java @@ -17,6 +17,8 @@ package org.elasticsoftware.akces.control; +import jakarta.annotation.Nullable; + import java.util.List; /** @@ -26,6 +28,11 @@ * aggregate services. It defaults to {@link AggregateServiceType#STANDARD} for records * that pre-date the introduction of this field (see {@link #effectiveType()}). * + *

The optional {@link #description()} field provides a human-readable summary of + * the aggregate, sourced from the {@code @AggregateInfo} or {@code @AgenticAggregateInfo} + * annotation. It may be {@code null} for legacy records or when no description was + * specified in the annotation. + * * @param aggregateName the logical name of the aggregate * @param commandTopic the Kafka topic to which commands for this aggregate are sent * @param domainEventTopic the Kafka topic on which this aggregate publishes domain events @@ -33,6 +40,7 @@ * @param supportedCommands the command types supported by this service * @param producedEvents the domain-event types produced by this service * @param consumedEvents the external domain-event types consumed by this service + * @param description a human-readable description of the aggregate; may be {@code null} */ public record AggregateServiceRecord( String aggregateName, @@ -41,7 +49,8 @@ public record AggregateServiceRecord( AggregateServiceType type, List supportedCommands, List producedEvents, - List consumedEvents + List consumedEvents, + @Nullable String description ) implements AkcesControlRecord { /** From 803a396ff2de2adfd7c81a46463fd585a08ce719 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:25:12 +0000 Subject: [PATCH 3/3] fix: add missing CommandInfo and DomainEventInfo imports to AkcesAgenticAggregateController Agent-Logs-Url: https://github.com/elasticsoftwarefoundation/akces-framework/sessions/1c4014cc-cfcd-49c7-92ad-325f11a8dd05 Co-authored-by: jwijgerd <914840+jwijgerd@users.noreply.github.com> --- .../akces/agentic/runtime/AkcesAgenticAggregateController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java index 8a330b45..ca928674 100644 --- a/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java +++ b/main/agentic/src/main/java/org/elasticsoftware/akces/agentic/runtime/AkcesAgenticAggregateController.java @@ -28,6 +28,8 @@ import org.elasticsoftware.akces.agentic.AgenticAggregateRuntime; import org.elasticsoftware.akces.aggregate.CommandType; import org.elasticsoftware.akces.aggregate.DomainEventType; +import org.elasticsoftware.akces.annotations.CommandInfo; +import org.elasticsoftware.akces.annotations.DomainEventInfo; import org.elasticsoftware.akces.control.AggregateServiceCommandType; import org.elasticsoftware.akces.control.AggregateServiceDomainEventType; import org.elasticsoftware.akces.control.AggregateServiceRecord;